Passed
Push — master ( 545dab...a27896 )
by Divine Niiquaye
12:03
created

RouteHandler::resolveHandler()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 11
nc 9
nop 2
dl 0
loc 23
ccs 12
cts 12
cp 1
crap 6
rs 9.2222
c 0
b 0
f 0

1 Method

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