Test Failed
Branch master (4153a4)
by Divine Niiquaye
13:02
created

RouteInvoker::resolveRoute()   C

Complexity

Conditions 16
Paths 133

Size

Total Lines 48
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 32.384

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 16
eloc 24
nc 133
nop 4
dl 0
loc 48
rs 5.2916
c 1
b 0
f 0
ccs 15
cts 25
cp 0.6
crap 32.384

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
3
/*
4
 * This file is part of Flight Routing.
5
 *
6
 * PHP version 7.4 and above required
7
 *
8
 * @author    Divine Niiquaye Ibok <[email protected]>
9
 * @copyright 2019 Biurad Group (https://biurad.com/)
10
 * @license   https://opensource.org/licenses/BSD-3-Clause License
11
 *
12
 * For the full copyright and license information, please view the LICENSE
13
 * file that was distributed with this source code.
14
 */
15
16
namespace Flight\Routing\Handlers;
17
18
use Flight\Routing\Exceptions\InvalidControllerException;
19
use Psr\Container\ContainerInterface;
20
use Psr\Http\Message\{ResponseInterface, ServerRequestInterface};
21
use Psr\Http\Server\RequestHandlerInterface;
22
23
/**
24
 * Invokes a route's handler with arguments.
25
 *
26
 * If you're using this library with Rade-DI, Yii Inject, DivineNii PHP Invoker, or Laravel DI,
27
 * instead of using this class as callable, use the call method from the container's class.
28
 *
29
 * @author Divine Niiquaye Ibok <[email protected]>
30
 */
31
class RouteInvoker
32
{
33
    public function __construct(protected ?ContainerInterface $container = null)
34
    {
35 99
    }
36
37 99
    /**
38
     * Auto-configure route handler parameters.
39
     *
40
     * @param array<string,mixed> $arguments
41
     */
42
    public function __invoke(mixed $handler, array $arguments): mixed
43
    {
44
        if (\is_string($handler)) {
45
            $handler = \ltrim($handler, '\\');
46
47
            if ($this->container?->has($handler)) {
48 68
                $handler = $this->container->get($handler);
49
            } elseif (\str_contains($handler, '@')) {
50 68
                $handler = \explode('@', $handler, 2);
51 12
                goto maybe_callable;
52
            } elseif (\class_exists($handler)) {
53 12
                $handler = \is_callable($this->container) ? ($this->container)($handler) : new $handler();
54 1
            }
55 1
        } elseif (\is_array($handler) && ([0, 1] === \array_keys($handler) && \is_string($handler[0]))) {
56 11
            $handler[0] = \ltrim($handler[0], '\\');
57 11
58
            maybe_callable:
59 56
            if ($this->container?->has($handler[0])) {
60
                $handler[0] = $this->container->get($handler[0]);
61 3
            } elseif (\class_exists($handler[0])) {
62
                $handler[0] = \is_callable($this->container) ? ($this->container)($handler[0]) : new $handler[0]();
63 3
            }
64 2
        }
65
66
        if (!\is_callable($handler)) {
67
            if (!\is_object($handler)) {
68 68
                throw new InvalidControllerException(\sprintf('Route has an invalid handler type of "%s".', \gettype($handler)));
69 13
            }
70 2
71
            return $handler;
72
        }
73 11
74
        $handlerRef = new \ReflectionFunction(\Closure::fromCallable($handler));
75
76 55
        if ($handlerRef->getNumberOfParameters() > 0) {
77
            $resolvedParameters = $this->resolveParameters($handlerRef->getParameters(), $arguments);
78 55
        }
79 54
80
        return $handlerRef->invokeArgs($resolvedParameters ?? []);
81
    }
82 55
83
    public function getContainer(): ?ContainerInterface
84
    {
85
        return $this->container;
86
    }
87
88
    /**
89
     * Resolve route handler & parameters.
90
     *
91 54
     * @throws InvalidControllerException
92
     */
93 54
    public static function resolveRoute(
94
        ServerRequestInterface $request,
95 54
        callable $resolver,
96 54
        mixed $handler,
97
        callable $arguments,
98 54
    ): ResponseInterface|FileHandler|string|null {
99
        if ($handler instanceof RequestHandlerInterface) {
100
            return $handler->handle($request);
101
        }
102
        $printed = \ob_start(); // Start buffering response output
103
104
        try {
105
            if ($handler instanceof ResourceHandler) {
106
                $handler = $handler($request->getMethod(), true);
107
            }
108
109
            $response = ($resolver)($handler, $arguments($request));
110
        } catch (\Throwable $e) {
111
            \ob_get_clean();
112 54
113 52
            throw $e;
114 43
        } finally {
115
            while (\ob_get_level() > 1) {
116 43
                $printed = \ob_get_clean() ?: null;
117
            } // If more than one output buffers is set ...
118
        }
119 9
120
        if ($response instanceof ResponseInterface || \is_string($response = $printed ?: ($response ?? \ob_get_clean()))) {
121
            return $response;
122
        }
123
124
        if ($response instanceof RequestHandlerInterface) {
125
            return $response->handle($request);
126 11
        }
127 5
128 6
        if ($response instanceof \Stringable) {
129
            return $response->__toString();
130 6
        }
131 1
132
        if ($response instanceof FileHandler) {
133
            return $response;
134
        }
135 54
136
        if ($response instanceof \JsonSerializable || $response instanceof \iterable || \is_array($response)) {
137
            return \json_encode($response, \JSON_THROW_ON_ERROR) ?: null;
138
        }
139
140
        return null;
141
    }
142
143
    /**
144
     * @param array<int,\ReflectionParameter> $refParameters
145
     * @param array<string,mixed>             $arguments
146
     *
147
     * @return array<int,mixed>
148
     */
149
    protected function resolveParameters(array $refParameters, array $arguments): array
150
    {
151
        $parameters = [];
152
        $nullable = 0;
153
154
        foreach ($arguments as $k => $v) {
155
            if (\is_numeric($k) || !\str_contains($k, '&')) {
156
                continue;
157
            }
158
159
            foreach (\explode('&', $k) as $i) {
160
                $arguments[$i] = $v;
161
            }
162
        }
163
164
        foreach ($refParameters as $index => $parameter) {
165
            $typeHint = $parameter->getType();
166
167
            if ($nullable > 0) {
168
                $index = $parameter->getName();
169
            }
170
171
            if ($typeHint instanceof \ReflectionUnionType || $typeHint instanceof \ReflectionIntersectionType) {
172
                foreach ($typeHint->getTypes() as $unionType) {
173
                    if ($unionType->isBuiltin()) {
174
                        continue;
175
                    }
176
177
                    if (isset($arguments[$unionType->getName()])) {
178
                        $parameters[$index] = $arguments[$unionType->getName()];
179
                        continue 2;
180
                    }
181
182
                    if ($this->container?->has($unionType->getName())) {
183
                        $parameters[$index] = $this->container->get($unionType->getName());
184
                        continue 2;
185
                    }
186
                }
187
            } elseif ($typeHint instanceof \ReflectionNamedType && !$typeHint->isBuiltin()) {
188
                if (isset($arguments[$typeHint->getName()])) {
189
                    $parameters[$index] = $arguments[$typeHint->getName()];
190
                    continue;
191
                }
192
193
                if ($this->container?->has($typeHint->getName())) {
194
                    $parameters[$index] = $this->container->get($typeHint->getName());
195
                    continue;
196
                }
197
            }
198
199
            if (isset($arguments[$parameter->getName()])) {
200
                $parameters[$index] = $arguments[$parameter->getName()];
201
            } elseif ($parameter->isDefaultValueAvailable()) {
202
                ++$nullable;
203
            } elseif (!$parameter->isVariadic() && ($parameter->isOptional() || $parameter->allowsNull())) {
204
                $parameters[$index] = null;
205
            }
206
        }
207
208
        return $parameters;
209
    }
210
}
211