Passed
Push — master ( 049287...68f58a )
by Divine Niiquaye
03:27
created

RouteHandler::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
c 1
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\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\,\"\:\[\]]+\}|\["[\w\"\,]+\]|\<(?|\?(xml)|\w+).*>.*<\/(\w+)>)$#s';
43
44
    protected ResponseFactoryInterface $responseFactory;
45
46
    /** @var callable */
47
    protected $handlerResolver;
48
49 90
    public function __construct(ResponseFactoryInterface $responseFactory, callable $handlerResolver = null)
50
    {
51 90
        $this->responseFactory = $responseFactory;
52 90
        $this->handlerResolver = $handlerResolver ?? new RouteInvoker();
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     *
58
     * @throws RouteNotFoundException|InvalidControllerException
59
     */
60 85
    public function handle(ServerRequestInterface $request): ResponseInterface
61
    {
62 85
        $route = $request->getAttribute(Route::class);
63
64 85
        if (!$route instanceof Route) {
65 25
            if (true === $notFoundResponse = $request->getAttribute(static::OVERRIDE_HTTP_RESPONSE)) {
66 10
                return $this->responseFactory->createResponse();
67
            }
68
69 16
            if ($notFoundResponse instanceof ResponseInterface) {
70 1
                return $notFoundResponse;
71
            }
72
73 15
            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 60
        if (!$response = $this->resolveRoute($route, $request)) {
78 1
            throw new InvalidControllerException('The route handler\'s content is not a valid PSR7 response body stream.');
79
        }
80
81 56
        if (!$response instanceof ResponseInterface) {
82 10
            ($result = $this->responseFactory->createResponse())->getBody()->write($response);
83 10
            $response = $result;
84
        }
85
86 56
        return $response->hasHeader(self::CONTENT_TYPE) ? $response : $this->negotiateContentType($response);
87
    }
88
89
    /**
90
     * @return ResponseInterface|string|false
91
     */
92 60
    protected function resolveRoute(Route $route, ServerRequestInterface $request)
93
    {
94 60
        \ob_start(); // Start buffering response output
95
96
        try {
97
            // The route handler to resolve ...
98 60
            $response = $this->resolveHandler($request, $route);
99
100 57
            if ($response instanceof ResponseInterface || \is_string($response)) {
101 50
                return $response;
102
            }
103
104 7
            if ($response instanceof \JsonSerializable || \is_iterable($response) || \is_array($response)) {
105 7
                return \json_encode($response, \JSON_THROW_ON_ERROR);
106
            }
107 3
        } catch (\Throwable $e) {
108 3
            \ob_get_clean();
109
110 3
            throw $e;
111 6
        } finally {
112 60
            while (\ob_get_level() > 1) {
113 57
                $response = \ob_get_clean(); // If more than one output buffers is set ...
114
            }
115
        }
116
117 6
        return $response ?? \ob_get_clean();
118
    }
119
120
    /**
121
     * A HTTP response Content-Type header negotiator for html, json, svg, xml, and plain-text.
122
     */
123 55
    protected function negotiateContentType(ResponseInterface $response): ResponseInterface
124
    {
125 55
        $contents = (string) $response->getBody();
126 55
        $contentType = 'text/html; charset=utf-8'; // Default content type.
127
128 55
        if (1 === $matched = \preg_match(static::CONTENT_REGEX, $contents, $matches, \PREG_UNMATCHED_AS_NULL)) {
129 5
            if (null === $matches[2]) {
130 1
                $contentType = 'application/json';
131 4
            } elseif ('xml' === $matches[1]) {
132 5
                $contentType = 'svg' === $matches[2] ? 'image/svg+xml' : \sprintf('application/%s; charset=utf-8', 'rss' === $matches[2] ? 'rss+xml' : 'xml');
133
            }
134 50
        } elseif (0 === $matched) {
135 46
            $contentType = 'text/plain; charset=utf-8';
136
        }
137
138 55
        return $response->withHeader(self::CONTENT_TYPE, $contentType);
139
    }
140
141
    /**
142
     * @return mixed
143
     */
144 60
    protected function resolveHandler(ServerRequestInterface $request, Route $route)
145
    {
146 60
        $handler = $route->getHandler();
147
148 60
        if ($handler instanceof RequestHandlerInterface) {
149 3
            return $handler->handle($request);
150
        }
151
152 57
        if (!$handler instanceof ResponseInterface) {
153 56
            $parameters = $this->resolveArguments($request, $route);
154
155 56
            if ($handler instanceof ResourceHandler) {
156 3
                $handler = $handler($request->getMethod());
157
            }
158
159 56
            $handler = ($this->handlerResolver)($handler, $parameters);
160
161 53
            if ($handler instanceof RequestHandlerInterface) {
162 5
                return $handler->handle($request);
163
            }
164
        }
165
166 49
        return true === $handler ? null : $handler;
167
    }
168
169
    /**
170
     * @return array<int|string,mixed>
171
     */
172 56
    protected function resolveArguments(ServerRequestInterface $request, Route $route): array
173
    {
174 56
        $parameters = $route->getArguments();
175
176 56
        foreach ([$request, $this->responseFactory] as $psr7) {
177 56
            $parameters[\get_class($psr7)] = $psr7;
178
179 56
            foreach ((@\class_implements($psr7) ?: []) as $psr7Interface) {
180 56
                $parameters[$psr7Interface] = $psr7;
181
            }
182
        }
183
184 56
        return $parameters;
185
    }
186
}
187