Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
98.36% |
60 / 61 |
|
90.00% |
9 / 10 |
CRAP | |
0.00% |
0 / 1 |
| Router | |
98.36% |
60 / 61 |
|
90.00% |
9 / 10 |
29 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| currentRoute | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| currentSegment | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addPrefixVariable | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| matchCurrentRoute | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
7 | |||
| match | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
6 | |||
| url | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
7 | |||
| add | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| routes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| prefixVariables | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace Dynart\Micro; |
| 4 | |
| 5 | class Router implements RouterInterface |
| 6 | { |
| 7 | /** |
| 8 | * Constant used for the case when no route found |
| 9 | */ |
| 10 | const ROUTE_NOT_FOUND = [null, null]; |
| 11 | |
| 12 | const CONFIG_INDEX_FILE = 'router.index_file'; |
| 13 | const CONFIG_ROUTE_PARAMETER = 'router.route_parameter'; |
| 14 | const CONFIG_USE_REWRITE = 'router.use_rewrite'; |
| 15 | |
| 16 | const DEFAULT_INDEX_FILE = 'index.php'; |
| 17 | const DEFAULT_ROUTE_PARAMETER = 'route'; |
| 18 | const DEFAULT_USE_REWRITE = false; |
| 19 | |
| 20 | /** |
| 21 | * Stores all of the routes in ['HTTP method' => ['/route' => callable]] format |
| 22 | * |
| 23 | * Callables: https://www.php.net/manual/en/language.types.callable.php |
| 24 | * But you can use the [ExampleClass::class, 'exampleMethod'] format too. |
| 25 | */ |
| 26 | protected array $routes = []; |
| 27 | |
| 28 | /** |
| 29 | * Stores callables for the start segments of all the routes. For example the locale is a |
| 30 | * prefix variable, because it is always in every route and the value depends on the incoming |
| 31 | * request. |
| 32 | * |
| 33 | * In the route '/en/books' the '/en' is a prefix variable and the '/books' is the stored route |
| 34 | */ |
| 35 | protected array $prefixVariables = []; |
| 36 | |
| 37 | /** |
| 38 | * All segments of the current route |
| 39 | * |
| 40 | * For example if the route is '/en/book/123/save' |
| 41 | * this will have the following value: ['en', 'book', '123', 'save'] |
| 42 | */ |
| 43 | protected array $segments = []; |
| 44 | |
| 45 | /** |
| 46 | * Fill up the `$segments` array |
| 47 | */ |
| 48 | public function __construct(protected ConfigInterface $config, protected RequestInterface $request) { |
| 49 | $this->segments = explode('/', $this->currentRoute()); |
| 50 | array_shift($this->segments); |
| 51 | } |
| 52 | |
| 53 | public function currentRoute(): string { |
| 54 | $routeParameter = $this->config->get(self::CONFIG_ROUTE_PARAMETER, self::DEFAULT_ROUTE_PARAMETER); |
| 55 | return $this->request->get($routeParameter, '/'); |
| 56 | } |
| 57 | |
| 58 | public function currentSegment(int $index, mixed $default = null): mixed { |
| 59 | return $this->segments[$index] ?? $default; |
| 60 | } |
| 61 | |
| 62 | public function addPrefixVariable(callable|array $callable): int { |
| 63 | $this->prefixVariables[] = $callable; |
| 64 | return count($this->prefixVariables) - 1; |
| 65 | } |
| 66 | |
| 67 | public function matchCurrentRoute(): array { |
| 68 | $method = $this->request->httpMethod(); |
| 69 | $routes = array_key_exists($method, $this->routes) ? $this->routes[$method] : []; |
| 70 | $segments = $this->segments; |
| 71 | foreach ($this->prefixVariables as $prefixVariable) { // remove prefix variables from the segments |
| 72 | array_shift($segments); |
| 73 | } |
| 74 | $segmentsCount = count($segments); |
| 75 | if (!$segmentsCount && isset($this->routes[$method]['/'])) { // if no segments and having home route |
| 76 | return [$this->routes[$method]['/'], []]; // return with that |
| 77 | } |
| 78 | $found = self::ROUTE_NOT_FOUND; |
| 79 | foreach ($routes as $route => $callable) { |
| 80 | $found = $this->match($route, $callable, $segments, $segmentsCount); |
| 81 | if ($found[0]) { |
| 82 | break; |
| 83 | } |
| 84 | } |
| 85 | return $found; |
| 86 | } |
| 87 | |
| 88 | /** |
| 89 | * Matches a route and returns with the given callable |
| 90 | * |
| 91 | * The `$callable` can be in a [Example::class, 'exampleMethod'] format as well, |
| 92 | * so you don't have to create an instance only when this callable is used. |
| 93 | */ |
| 94 | protected function match(string $route, callable|array $callable, array $currentParts, int $currentPartsCount): array { |
| 95 | $parts = explode('/', $route); |
| 96 | array_shift($parts); |
| 97 | if (count($parts) != $currentPartsCount) { |
| 98 | return self::ROUTE_NOT_FOUND; |
| 99 | } |
| 100 | $found = true; |
| 101 | $params = []; |
| 102 | foreach ($parts as $i => $part) { |
| 103 | if ($part == $currentParts[$i]) { |
| 104 | continue; |
| 105 | } |
| 106 | if ($part == '?') { |
| 107 | $params[] = $currentParts[$i]; |
| 108 | continue; |
| 109 | } |
| 110 | $found = false; |
| 111 | break; |
| 112 | } |
| 113 | if ($found) { |
| 114 | return [$callable, $params]; |
| 115 | } |
| 116 | return self::ROUTE_NOT_FOUND; |
| 117 | } |
| 118 | |
| 119 | public function url(?string $route = null, array $params = [], string $amp = '&'): string { |
| 120 | $prefix = ''; |
| 121 | foreach ($this->prefixVariables as $callable) { |
| 122 | $prefix .= '/'.call_user_func($callable); |
| 123 | } |
| 124 | $result = $this->config->get(AbstractApp::CONFIG_BASE_URL); |
| 125 | $useRewrite = $this->config->get(self::CONFIG_USE_REWRITE, self::DEFAULT_USE_REWRITE); |
| 126 | if ($useRewrite) { |
| 127 | $result .= $route == null ? '' : $prefix.$route; |
| 128 | } else { |
| 129 | $indexFile = $this->config->get(self::CONFIG_INDEX_FILE, self::DEFAULT_INDEX_FILE); |
| 130 | $result .= '/'.$indexFile; |
| 131 | if ($route && $route != '/') { |
| 132 | $routeParameter = $this->config->get(self::CONFIG_ROUTE_PARAMETER, self::DEFAULT_ROUTE_PARAMETER); |
| 133 | $params[$routeParameter] = $prefix.$route; |
| 134 | } |
| 135 | } |
| 136 | if ($params) { |
| 137 | $result .= '?'.http_build_query($params, '', $amp); |
| 138 | } |
| 139 | return str_replace('%2F', '/', $result); |
| 140 | } |
| 141 | |
| 142 | public function add(string $route, callable|array $callable, string $method = 'GET'): void { |
| 143 | if ($method == 'BOTH') { |
| 144 | $this->add($route, $callable); // GET |
| 145 | $this->add($route, $callable, 'POST'); |
| 146 | } else { |
| 147 | if (!array_key_exists($method, $this->routes)) { |
| 148 | $this->routes[$method] = []; |
| 149 | } |
| 150 | $this->routes[$method][$route] = $callable; |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | public function routes(): array { |
| 155 | return $this->routes; |
| 156 | } |
| 157 | |
| 158 | public function prefixVariables(): array { |
| 159 | return $this->prefixVariables; |
| 160 | } |
| 161 | } |