Passed
Push — main ( 4bfbca...f41f09 )
by Thomas
03:11
created

Router::collectMiddleware()   B

Complexity

Conditions 8
Paths 1

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 8

Importance

Changes 0
Metric Value
eloc 21
c 0
b 0
f 0
dl 0
loc 35
ccs 22
cts 22
cp 1
rs 8.4444
cc 8
nc 1
nop 2
crap 8
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Conia\Chuck\Routing;
6
7
use Conia\Chuck\Dispatcher;
8
use Conia\Chuck\Exception\HttpMethodNotAllowed;
9
use Conia\Chuck\Exception\HttpNotFound;
10
use Conia\Chuck\Exception\RuntimeException;
11
use Conia\Chuck\MiddlewareInterface;
12
use Conia\Chuck\MiddlewareWrapper;
13
use Conia\Chuck\Registry\Registry;
14
use Conia\Chuck\Registry\Resolver;
15
use Conia\Chuck\Request;
16
use Conia\Chuck\Response;
17
use Conia\Chuck\View;
18
use Conia\Chuck\ViewHandler;
19
use Psr\Http\Server\MiddlewareInterface as PsrMiddlewareInterface;
20
use Throwable;
21
22
class Router
23
{
24
    use AddsRoutes;
25
    use AddsMiddleware;
26
27
    protected const ALL = 'ALL';
28
29
    protected ?Route $route = null;
30
31
    /** @psalm-var array<string, list<Route>> */
32
    protected array $routes = [];
33
34
    /** @var array<string, StaticRoute> */
35
    protected array $staticRoutes = [];
36
37
    /** @var array<string, Route> */
38
    protected array $names = [];
39
40 3
    public function getRoute(): Route
41
    {
42 3
        if (is_null($this->route)) {
43 1
            throw new RuntimeException('Route is not initialized');
44
        }
45
46 2
        return $this->route;
47
    }
48
49 60
    public function addRoute(Route $route): void
50
    {
51 60
        $name = $route->name();
52 60
        $noMethodGiven = true;
53
54 60
        if (array_key_exists($name, $this->names)) {
55 2
            throw new RuntimeException(
56 2
                'Duplicate route: ' . $name . '. If you want to use the same ' .
57 2
                    'url pattern with different methods, you have to create routes with names.'
58 2
            );
59
        }
60
61 60
        foreach ($route->methods() as $method) {
62 30
            $noMethodGiven = false;
63 30
            $this->routes[$method][] = $route;
64
        }
65
66 60
        if ($noMethodGiven) {
67 36
            $this->routes[self::ALL][] = $route;
68
        }
69
70 60
        $this->names[$name] = $route;
71
    }
72
73 3
    public function addGroup(Group $group): void
74
    {
75 3
        $group->create($this);
76
    }
77
78 6
    public function addStatic(
79
        string $prefix,
80
        string $dir,
81
        ?string $name = null,
82
    ): void {
83 6
        if (empty($name)) {
84 4
            $name = $prefix;
85
        }
86
87 6
        if (array_key_exists($name, $this->staticRoutes)) {
88 2
            throw new RuntimeException(
89 2
                'Duplicate static route: ' . $name . '. If you want to use the same ' .
90 2
                    'url prefix you have to create static routes with names.'
91 2
            );
92
        }
93
94 6
        if (is_dir($dir)) {
95 5
            $this->staticRoutes[$name] = new StaticRoute(
96 5
                prefix: '/' . trim($prefix, '/') . '/',
97 5
                dir: $dir,
98 5
            );
99
        } else {
100 1
            throw new RuntimeException("The static directory does not exist: {$dir}");
101
        }
102
    }
103
104 3
    public function staticUrl(
105
        string $name,
106
        string $path,
107
        bool $bust = false,
108
        ?string $host = null
109
    ): string {
110 3
        $route = $this->staticRoutes[$name];
111
112 3
        if ($bust) {
113
            // Check if there is already a query parameter present
114 1
            if (strpos($path, '?')) {
115 1
                $file = strtok($path, '?');
116 1
                $sep = '&';
117
            } else {
118 1
                $file = $path;
119 1
                $sep = '?';
120
            }
121
122 1
            $buster = $this->getCacheBuster($route->dir, $file);
123
124 1
            if (!empty($buster)) {
125 1
                $path .= $sep . 'v=' . $buster;
126
            }
127
        }
128
129 3
        return ($host ? trim($host, '/') : '') . $route->prefix . trim($path, '/');
130
    }
131
132 14
    public function routeUrl(string $__routeName__, mixed ...$args): string
133
    {
134 14
        $route = $this->names[$__routeName__] ?? null;
135
136 14
        if ($route) {
137 13
            return $route->url(...$args);
138
        }
139
140 1
        throw new RuntimeException('Route not found: ' . $__routeName__);
141
    }
142
143 47
    public function match(Request $request): Route
144
    {
145 47
        $url = $request->uri()->getPath();
146 47
        $requestMethod = $request->method();
147
148 47
        foreach ([$requestMethod, self::ALL] as $method) {
149 47
            foreach ($this->routes[$method] ?? [] as $route) {
150 46
                if ($route->match($url)) {
151 45
                    return $route;
152
                }
153
            }
154
        }
155
156
        // We know now, that the route does not match.
157
        // Check if it would match one of the remaining methods
158 9
        $wrongMethod = false;
159 9
        $remainingMethods = array_keys($this->routes);
160
161 9
        foreach ([$requestMethod, self::ALL] as $method) {
162 9
            if (($key = array_search($method, $remainingMethods)) !== false) {
163 4
                unset($remainingMethods[$key]);
164
            }
165
        }
166
167 9
        foreach ($remainingMethods as $method) {
168 7
            foreach ($this->routes[$method] as $route) {
169 7
                if ($route->match($url)) {
170 7
                    $wrongMethod = true;
171
172 7
                    break;
173
                }
174
            }
175
        }
176
177 9
        if ($wrongMethod) {
178 7
            throw new HttpMethodNotAllowed();
179
        }
180
181 2
        throw new HttpNotFound();
182
    }
183
184
    /**
185
     * Looks up the matching route and generates the response.
186
     */
187 26
    public function dispatch(Request $request, Registry $registry): Response
188
    {
189 26
        $this->route = $this->match($request);
190
191 25
        $view = new View($this->route->view(), $this->route->args(), $registry);
192 23
        $queue = $this->collectMiddleware($view, $registry);
193 22
        $queue[] = new ViewHandler($view, $registry, $this->route);
194
195 22
        return (new Dispatcher($queue, $registry))->dispatch($request);
196
    }
197
198 1
    protected function getCacheBuster(string $dir, string $path): string
199
    {
200 1
        $ds = DIRECTORY_SEPARATOR;
201 1
        $file = $dir . $ds . ltrim(str_replace('/', $ds, $path), $ds);
202
203
        try {
204 1
            return hash('xxh32', (string)filemtime($file));
205 1
        } catch (Throwable) {
206 1
            return '';
207
        }
208
    }
209
210 23
    protected function collectMiddleware(View $view, Registry $registry): array
211
    {
212 23
        $middlewareAttributes = $view->attributes(MiddlewareInterface::class);
213
214 23
        return array_map(
215 23
            function (
216 23
                MiddlewareInterface|PsrMiddlewareInterface|callable|string $middleware
217 23
            ) use ($registry): MiddlewareInterface|PsrMiddlewareInterface {
218
                if (
219 6
                    ($middleware instanceof MiddlewareInterface)
220 6
                    || ($middleware instanceof PsrMiddlewareInterface)
221
                ) {
222 2
                    return $middleware;
223
                }
224
225 6
                if (is_string($middleware) && class_exists($middleware)) {
226 1
                    $object = (new Resolver($registry))->autowire($middleware);
227 1
                    assert($object instanceof MiddlewareInterface || $object instanceof PsrMiddlewareInterface);
228
229 1
                    return $object;
230
                }
231
232 5
                if (is_callable($middleware)) {
233
                    /** @psalm-var callable(Request, callable):Response $middleware */
234 4
                    return new MiddlewareWrapper($middleware);
235
                }
236
237 1
                throw new RuntimeException('Invalid middleware: ' .
238
                    /** @scrutinizer ignore-type */
239 1
                    print_r($middleware, true));
240 23
            },
241 23
            array_merge(
242 23
                $this->middleware,
243 23
                isset($this->route) ? $this->route->getMiddleware() : [],
0 ignored issues
show
Bug introduced by
The method getMiddleware() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

243
                isset($this->route) ? $this->route->/** @scrutinizer ignore-call */ getMiddleware() : [],

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
244 23
                $middlewareAttributes,
245 23
            )
246 23
        );
247
    }
248
}
249