Lines 63.86% 76 / 119
Methods 31.57% 6 / 19
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 registerRoute 0.00% 0 / 10 0.00% 0 / 1 12
 loadRoutes 84.61% 11 / 13 0.00% 0 / 1 4.06
 [Lucent\Router] setDefaultController 0.00% 0 / 1 0.00% 0 / 1 2
 [Lucent\Router] resolveController 0.00% 0 / 1 0.00% 0 / 1 2
 [Lucent\Router] group 0.00% 0 / 17 0.00% 0 / 1 6
 [Lucent\Router] parseRouteParameter 100.00% 3 / 3 100.00% 1 / 1 2
 [Lucent\Router] disable 100.00% 4 / 4 100.00% 1 / 1 3
 [Lucent\Router] isDisabled 100.00% 1 / 1 100.00% 1 / 1 2
 [Lucent\Router] getUriAsArray 68.75% 11 / 16 0.00% 0 / 1 7.10
 [Lucent\Router] analyseRouteAndLookup 100.00% 8 / 8 100.00% 1 / 1 5
 [Lucent\Router] matchRoute 100.00% 24 / 24 100.00% 1 / 1 6
 [Lucent\Router] setActiveMiddleware 0.00% 0 / 1 0.00% 0 / 1 2
 [Lucent\Router] setPrefix 0.00% 0 / 1 0.00% 0 / 1 2
 [Lucent\Router] getPrefix 0.00% 0 / 1 0.00% 0 / 1 2
 [Lucent\Router] getMiddleware 0.00% 0 / 1 0.00% 0 / 1 2
 [Lucent\Router] getFullPath 0.00% 0 / 1 0.00% 0 / 1 6
 [Lucent\Router] getFullClassName 0.00% 0 / 1 0.00% 0 / 1 6
 [Lucent\Router] reset 100.00% 7 / 7 100.00% 1 / 1 1
 [Lucent\Router] getRoutes 87.50% 7 / 8 0.00% 0 / 1 5.05
14class HttpRouter extends Router
15{
16    /**
17     * Register a new route with optional controller
18     */
19    public function registerRoute(string $uri, string $type, string $method, ?string $controller = null, array $middleware = []): void
20    {
21        // Normalize the URI by trimming slashes and ensuring single slashes between segments
22        $uri = trim($uri, '/');
23        $prefix = trim($this->prefix ?? '', '/');
24
25        // Build the full URI with prefix
26        $fullUri = $prefix ? $prefix . '/' . $uri : $uri;
27
28        // Apply the namespace group attribute (if any) to the controller.
29        if ($controller !== null) {
30            $controller = $this->getFullClassName($controller);
31        }
32
33        // Store the route without leading slash for consistent comparison
34        $this->routes[$type][$fullUri] = [
35            "controller" => $controller,
36            "method" => $method,
37            "middleware" => array_merge($this->middleware, $middleware)
38        ];
39    }
40
41
42    /**
43     * Load routes from a file
44     * @throws FileNotFound
45     */
46    public function loadRoutes(string $file, ?string $prefix = null): void
47    {
48        // Detect absolute paths (e.g. from glob() in Application::boot()) so
49        // the File constructor doesn't prepend rootPath() a second time.
50        $file = new File($file, null, FileSystem::isAbsolute($file));
51
52        if (!$file->exists()) {
53            throw new HttpException(
54                HttpStatus::SERVER_ERROR,
55                "Route file not found",
56                new RuntimeException("File not found: $file->path")
57            );
58        }
59
60        //Setting prefix before loading routes
61        if ($prefix !== null) {
62            $previousPrefix = $this->prefix;
63            $this->prefix = $prefix;
64        }
65
66        try {
67            require_once $file->path;
68        } finally {
69            //Restore existing prefix even if the route file throws, so the
70            //prefix never leaks into subsequently registered routes.
71            if ($prefix !== null) {
72                $this->prefix = $previousPrefix;
73            }
74        }
75    }
76}

Inherited from Lucent\Router

44    public function setDefaultController(?string $controller): void
45    {
46        $this->defaultController = $controller;
47    }
52    protected function resolveController(?string $routeController): ?string
53    {
54        return $routeController ?? $this->defaultController;
55    }
60    public function group(array $attributes, callable $callback): void
61    {
62        $this->groupStack[] = [
63            'middleware' => $this->middleware,
64            'prefix' => $this->prefix,
65            'namespace' => $this->namespace,
66            'defaultController' => $this->defaultController
67        ];
68
69        // Merge new group attributes
70        $this->middleware = array_merge($this->middleware, $attributes['middleware'] ?? []);
71        // Append the new prefix onto the existing one, preserving the
72        // accumulation behaviour for nested groups (e.g. /api + /v1 = /api/v1).
73        // When no prefix is provided, keep the current value (which may be
74        // null) rather than converting it to an empty string.
75        if (array_key_exists('prefix', $attributes)) {
76            $this->prefix = ($this->prefix ?? '') . $attributes['prefix'];
77        }
78        // else: $this->prefix unchanged (preserves null distinct from '')
79        $this->namespace = $attributes['namespace'] ?? $this->namespace;
80        $this->defaultController = $attributes['defaultController'] ?? $this->defaultController;
81
82        // Execute the group's route definitions
83        $callback($this);
84
85        // Restore previous group attributes
86        $previous = array_pop($this->groupStack);
87        $this->middleware = $previous['middleware'];
88        $this->prefix = $previous['prefix'];
89        $this->namespace = $previous['namespace'];
90        $this->defaultController = $previous['defaultController'];
91    }
97    protected function parseRouteParameter(string $segment): array
98    {
99        if (preg_match('/^\{([a-zA-Z_][a-zA-Z0-9_]*)\}$/', $segment, $matches)) {
100            return [true, $matches[1]];
101        }
102        return [false, ''];
103    }
109    public function disable(string|array $route): void
110    {
111        if (is_array($route)) {
112            foreach ($route as $r) {
113                $this->disabled[$r] = true;
114            }
115        } else {
116            $this->disabled[$route] = true;
117        }
118    }
123    protected function isDisabled(string $uri): bool
124    {
125        return ($this->disabled["*"] ?? false) || ($this->disabled[$uri] ?? false);
126    }
134    public function getUriAsArray(?string $url = null, string $separator = "/"): array
135    {
136        // If no URL provided, check for CLI arguments
137        if ($url === null) {
138            // Check if running in CLI mode
139            if (php_sapi_name() === 'cli') {
140                // Use global $_SERVER['argv'] for CLI arguments
141                $url = implode(' ', array_slice($_SERVER['argv'], 1));
142            } else {
143                $url = $_SERVER["REQUEST_URI"] ?? '';
144            }
145        }
146
147        // Remove any protocol, domain, or query string
148        $url = parse_url($url, PHP_URL_PATH);
149        if ($url === false || $url === null) {
150            $url = '';
151        }
152
153        // Decode URL to handle special characters
154        $url = urldecode($url);
155
156        // Remove query string if present (must use !== false - strpos returns 0
157        // when '?' is the first character, which is falsy)
158        $pos = strpos($url, "?");
159        if ($pos !== false) {
160            $url = substr($url, 0, $pos);
161        }
162
163        // Normalize URL for CLI
164        $url = preg_replace('/\s+/', $separator, trim($url));
165
166        // Normalize slashes and trim
167        $url = trim($url, '/');
168
169        // Split and filter empty segments
170        return array_values(array_filter(explode($separator, $url), function($segment) {
171            return $segment !== '';
172        }));
173    }
177    public function analyseRouteAndLookup(array $route, string $method): array
178    {
179        if (!isset($this->routes[$method])) {
180            throw new HttpException(HttpStatus::NOT_FOUND);
181        }
182
183        foreach ($this->routes[$method] as $key => $routeDefinition) {
184            if ($match = $this->matchRoute($key, $route, '/', $routeDefinition, $method)) {
185                if ($this->isDisabled($key)) {
186                    throw new HttpException(HttpStatus::FORBIDDEN);
187                }
188                return $match;
189            }
190        }
191
192
193        throw new HttpException(HttpStatus::NOT_FOUND);
194    }
199    protected function matchRoute(string $routePath, array $uri, string $separator, array $route, string $method): ?array
200    {
201        // Normalize route path
202        $routePath = trim($routePath, '/');
203        $routeSegments = $this->getUriAsArray($routePath, $separator);
204
205        if (count($routeSegments) !== count($uri)) {
206            return null;
207        }
208
209        $variables = [];
210        $matches = true;
211
212        for ($i = 0; $i < count($routeSegments); $i++) {
213            [$isParameter, $paramName] = $this->parseRouteParameter($routeSegments[$i]);
214
215            if (!$isParameter) {
216                if ($routeSegments[$i] !== $uri[$i]) {
217                    $matches = false;
218                    break;
219                }
220            } else {
221                $variables[$paramName] = $uri[$i];
222            }
223        }
224
225        if (!$matches) {
226            return null;
227        }
228
229        Log::channel('lucent.routing')->info("[Router] Found route : " . $routePath."\n    Http Method:".$method."\n    Http Controller: {$route["controller"]}@{$route["method"]}");
230
231        return [
232            "route" => $routePath,
233            "outcome" => true,
234            "controller" => $route["controller"],
235            "method" => $route["method"],
236            "variables" => $variables,
237            "middleware" => $route["middleware"] ?? []
238        ];
239    }
244    public function setActiveMiddleware(array $middleware): void
245    {
246        $this->middleware = $middleware;
247    }
252    public function setPrefix(?string $prefix): void
253    {
254        $this->prefix = $prefix;
255    }
260    public function getPrefix(): ?string
261    {
262        return $this->prefix;
263    }
268    public function getMiddleware(): array
269    {
270        return $this->middleware;
271    }
276    protected function getFullPath(string $path): string
277    {
278        return $this->prefix ? rtrim($this->prefix, '/') . '/' . ltrim($path, '/') : $path;
279    }
284    protected function getFullClassName(string $controller): string
285    {
286        return $this->namespace ? rtrim($this->namespace, '\\') . '\\' . $controller : $controller;
287    }
289    public function reset(): void
290    {
291        $this->routes = [];
292        $this->groupStack = [];
293        $this->middleware = [];
294        $this->prefix = null;
295        $this->namespace = null;
296        $this->defaultController = null;
297        $this->disabled = ["*" => false];
298    }
300    public function getRoutes(bool $activeOnly = true): array
301    {
302        if ($activeOnly) {
303            $active = [];
304            foreach ($this->routes as $method => $routes) {
305                foreach ($routes as $uri => $route) {
306                    if (!$this->isDisabled($uri)) {
307                        $active[$method][$uri] = $route;
308                    }
309                }
310            }
311            return $active;
312        }
313
314        return $this->routes;
315    }