Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.78% |
159 / 166 |
|
70.00% |
7 / 10 |
CRAP | |
0.00% |
0 / 1 |
Http | |
95.78% |
159 / 166 |
|
70.00% |
7 / 10 |
71 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
6 | |||
getBasePath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
prepare | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
match | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
10 | |||
hasRoute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
noRouteFound | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
3.00 | |||
flattenRoutes | |
68.75% |
11 / 16 |
|
0.00% |
0 / 1 |
11.47 | |||
getRouteRegex | |
100.00% |
43 / 43 |
|
100.00% |
1 / 1 |
7 | |||
parseRouteParams | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
12.02 | |||
getUrl | |
100.00% |
36 / 36 |
|
100.00% |
1 / 1 |
19 |
1 | <?php |
2 | /** |
3 | * Pop PHP Framework (https://www.popphp.org/) |
4 | * |
5 | * @link https://github.com/popphp/popphp-framework |
6 | * @author Nick Sagona, III <dev@noladev.com> |
7 | * @copyright Copyright (c) 2009-2025 NOLA Interactive, LLC. |
8 | * @license https://www.popphp.org/license New BSD License |
9 | */ |
10 | |
11 | /** |
12 | * @namespace |
13 | */ |
14 | namespace Pop\Router\Match; |
15 | |
16 | /** |
17 | * Pop router HTTP match class |
18 | * |
19 | * @category Pop |
20 | * @package Pop\Router |
21 | * @author Nick Sagona, III <dev@noladev.com> |
22 | * @copyright Copyright (c) 2009-2025 NOLA Interactive, LLC. |
23 | * @license https://www.popphp.org/license New BSD License |
24 | * @version 4.3.5 |
25 | */ |
26 | class Http extends AbstractMatch |
27 | { |
28 | |
29 | /** |
30 | * Base path |
31 | * @var ?string |
32 | */ |
33 | protected ?string $basePath = null; |
34 | |
35 | /** |
36 | * Constructor |
37 | * |
38 | * Instantiate the HTTP match object |
39 | */ |
40 | public function __construct() |
41 | { |
42 | $basePath = str_replace([realpath($_SERVER['DOCUMENT_ROOT']), '\\'], ['', '/'], realpath(getcwd())); |
43 | $this->basePath = !empty($basePath) ? $basePath : ''; |
44 | $trailingSlash = null; |
45 | |
46 | $path = ($this->basePath != '') ? |
47 | substr($_SERVER['REQUEST_URI'], strlen($this->basePath)) : $_SERVER['REQUEST_URI']; |
48 | |
49 | // Trim query string, if present |
50 | if (strpos($path, '?')) { |
51 | $path = substr($path, 0, strpos($path, '?')); |
52 | } |
53 | |
54 | // Trim trailing slash, if present |
55 | if (str_ends_with($path, '/')) { |
56 | $path = substr($path, 0, -1); |
57 | $trailingSlash = '/'; |
58 | } |
59 | |
60 | if ($path == '') { |
61 | $this->segments = ['index']; |
62 | $this->routeString = '/'; |
63 | } else { |
64 | $this->segments = explode('/', substr($path, 1)); |
65 | $this->routeString = '/' . implode('/', $this->segments) . $trailingSlash; |
66 | } |
67 | } |
68 | |
69 | /** |
70 | * Get the base path |
71 | * |
72 | * @return string |
73 | */ |
74 | public function getBasePath(): string |
75 | { |
76 | return $this->basePath; |
77 | } |
78 | |
79 | /** |
80 | * Prepare the routes |
81 | * |
82 | * @return static |
83 | */ |
84 | public function prepare(): static |
85 | { |
86 | $this->flattenRoutes($this->routes); |
87 | return $this; |
88 | } |
89 | |
90 | /** |
91 | * Match the route |
92 | * |
93 | * @param mixed $forceRoute |
94 | * @return bool |
95 | */ |
96 | public function match(mixed $forceRoute = null): bool |
97 | { |
98 | if (count($this->preparedRoutes) == 0) { |
99 | $this->prepare(); |
100 | } |
101 | |
102 | $routeToMatch = $forceRoute ?? $this->routeString; |
103 | $directMatch = null; |
104 | |
105 | if (array_key_exists($routeToMatch, $this->routes)) { |
106 | $directMatch = $routeToMatch; |
107 | } else if (array_key_exists($routeToMatch . '/', $this->routes)) { |
108 | $directMatch = $routeToMatch . '/'; |
109 | } else if (array_key_exists($routeToMatch . '[/]', $this->routes)) { |
110 | $directMatch = $routeToMatch . '[/]'; |
111 | } |
112 | |
113 | if ($directMatch !== null) { |
114 | foreach ($this->preparedRoutes as $regex => $controller) { |
115 | if ($directMatch == $controller['route']) { |
116 | $this->route = $regex; |
117 | break; |
118 | } |
119 | } |
120 | } else { |
121 | foreach ($this->preparedRoutes as $regex => $controller) { |
122 | if (preg_match($regex, $routeToMatch) != 0) { |
123 | $this->route = $regex; |
124 | break; |
125 | } |
126 | } |
127 | } |
128 | |
129 | $this->parseRouteParams(); |
130 | |
131 | return $this->hasRoute(); |
132 | } |
133 | |
134 | /** |
135 | * Determine if the route has been matched |
136 | * |
137 | * @return bool |
138 | */ |
139 | public function hasRoute(): bool |
140 | { |
141 | return ($this->route !== null) || ($this->dynamicRoute !== null) || ($this->defaultRoute !== null); |
142 | } |
143 | |
144 | /** |
145 | * Method to process if a route was not found |
146 | * |
147 | * @param bool $exit |
148 | * @return void |
149 | */ |
150 | public function noRouteFound(bool $exit = true): void |
151 | { |
152 | if (!headers_sent()) { |
153 | header('HTTP/1.1 404 Not Found'); |
154 | } |
155 | echo '<!DOCTYPE html>' . PHP_EOL; |
156 | echo '<html>' . PHP_EOL; |
157 | echo ' <head>' . PHP_EOL; |
158 | echo ' <title>Page Not Found</title>' . PHP_EOL; |
159 | echo ' </head>' . PHP_EOL; |
160 | echo '<body>' . PHP_EOL; |
161 | echo ' <h1>Page Not Found</h1>' . PHP_EOL; |
162 | echo '</body>' . PHP_EOL; |
163 | echo '</html>'. PHP_EOL; |
164 | |
165 | if ($exit) { |
166 | exit(); |
167 | } |
168 | } |
169 | |
170 | /** |
171 | * Flatten the nested routes |
172 | * |
173 | * @param array|string $route |
174 | * @param mixed $controller |
175 | * @return void |
176 | */ |
177 | protected function flattenRoutes(array|string $route, mixed $controller = null): void |
178 | { |
179 | if (is_array($route)) { |
180 | foreach ($route as $r => $c) { |
181 | $this->flattenRoutes($r, $c); |
182 | } |
183 | } else if ($controller !== null) { |
184 | if (!isset($controller['controller'])) { |
185 | foreach ($controller as $r => $c) { |
186 | $this->flattenRoutes($route . $r, $c); |
187 | } |
188 | } else { |
189 | $routeRegex = $this->getRouteRegex($route); |
190 | $this->preparedRoutes[$routeRegex['regex']] = array_merge($controller, [ |
191 | 'route' => $route, |
192 | 'params' => $routeRegex['params'], |
193 | ]); |
194 | if (isset($controller['default']) && ($controller['default'])) { |
195 | if (isset($controller['action'])) { |
196 | unset($controller['action']); |
197 | } |
198 | $this->defaultRoute['*'] = $controller; |
199 | } |
200 | } |
201 | } |
202 | } |
203 | |
204 | /** |
205 | * Get the REGEX pattern for the route string |
206 | * |
207 | * @param string $route |
208 | * @return array |
209 | */ |
210 | protected function getRouteRegex(string $route): array |
211 | { |
212 | $required = []; |
213 | $optional = []; |
214 | $params = []; |
215 | $offsets = []; |
216 | $paramArray = false; |
217 | |
218 | if (str_contains($route, '*')) { |
219 | $paramArray = true; |
220 | $route = str_replace('*', '', $route); |
221 | } |
222 | |
223 | preg_match_all('/\[\/\:[^\[]+\]/', $route, $optional, PREG_OFFSET_CAPTURE); |
224 | preg_match_all('/(?<!\[)\/\:+\w*/', $route, $required, PREG_OFFSET_CAPTURE); |
225 | |
226 | foreach ($required[0] as $req) { |
227 | $name = substr($req[0], (strpos($req[0], ':') + 1)); |
228 | $route = str_replace($req[0], '/.[a-zA-Z0-9_\.\-\p{L}]*', $route); |
229 | $offsets[] = $req[1]; |
230 | $params[] = [ |
231 | 'param' => $req[0], |
232 | 'name' => $name, |
233 | 'offset' => $req[1], |
234 | 'required' => true, |
235 | 'array' => false |
236 | ]; |
237 | } |
238 | |
239 | foreach ($optional[0] as $opt) { |
240 | $name = substr($opt[0], (strpos($opt[0], ':') + 1), -1); |
241 | $route = str_replace($opt[0], '(|/[a-zA-Z0-9_\.\-\p{L}]*)', $route); |
242 | $offsets[] = $opt[1]; |
243 | $params[] = [ |
244 | 'param' => $opt[0], |
245 | 'name' => $name, |
246 | 'offset' => $opt[1], |
247 | 'required' => false, |
248 | 'array' => false |
249 | ]; |
250 | } |
251 | |
252 | $route = '^' . str_replace('/', '\/', $route) . '$'; |
253 | if (str_ends_with($route, '[\/]$')) { |
254 | $route = str_replace('[\/]$', '(|\/)$', $route); |
255 | } |
256 | |
257 | array_multisort($offsets, SORT_ASC, $params); |
258 | |
259 | if (($paramArray) && (count($params) > 0)) { |
260 | $params[(count($params) - 1)]['array'] = true; |
261 | $route = str_replace('$', '.*', $route); |
262 | } |
263 | |
264 | return [ |
265 | 'regex' => '/' . $route . '/u', |
266 | 'params' => $params |
267 | ]; |
268 | } |
269 | |
270 | /** |
271 | * Parse route dispatch parameters |
272 | * |
273 | * @return void |
274 | */ |
275 | protected function parseRouteParams(): void |
276 | { |
277 | if (isset($this->preparedRoutes[$this->route]['params']) && |
278 | (count($this->preparedRoutes[$this->route]['params']) > 0)) { |
279 | $offset = 0; |
280 | foreach ($this->preparedRoutes[$this->route]['params'] as $i => $param) { |
281 | $value = substr($this->routeString, ($param['offset'] + $offset + 1)); |
282 | if ($param['array']) { |
283 | if (!$value) { |
284 | $value = []; |
285 | } else { |
286 | $value = (str_contains($value, '/')) ? explode('/', $value) : [$value]; |
287 | } |
288 | } else { |
289 | if (str_contains($value, '/')) { |
290 | $value = substr($value, 0, strpos($value, '/')); |
291 | $offset += strlen($value) - strlen($param['param']) + 1; |
292 | } else { |
293 | $offset += strlen($value) - strlen($param['param']) + 1; |
294 | } |
295 | } |
296 | if ($value != '') { |
297 | $this->routeParams[$param['name']] = $value; |
298 | } |
299 | } |
300 | } else if (($this->dynamicRoute !== null) && (count($this->segments) >= 3)) { |
301 | $this->routeParams = (str_contains($this->dynamicRoute, '/:param*')) ? |
302 | [array_slice($this->segments, 2)] : array_slice($this->segments, 2); |
303 | } |
304 | } |
305 | |
306 | /** |
307 | * Get URL for the named route |
308 | * |
309 | * @param string $routeName |
310 | * @param mixed $params |
311 | * @param bool $fqdn |
312 | * @return string |
313 | */ |
314 | public function getUrl(string $routeName, mixed $params = null, bool $fqdn = false): string |
315 | { |
316 | $url = ''; |
317 | $baseUrl = ''; |
318 | |
319 | if ($fqdn) { |
320 | $baseUrl .= (isset($_SERVER['SERVER_PORT']) && ($_SERVER['SERVER_PORT'] == 443)) ? 'https://' : 'http://'; |
321 | if (isset($_SERVER['HTTP_HOST'])) { |
322 | $baseUrl .= $_SERVER['HTTP_HOST']; |
323 | } |
324 | } |
325 | |
326 | $baseUrl .= $this->basePath; |
327 | |
328 | if (isset($this->routeNames[$routeName]) && isset($this->routes[$this->routeNames[$routeName]])) { |
329 | $route = $this->routeNames[$routeName]; |
330 | $preparedRoute = null; |
331 | |
332 | if (count($this->preparedRoutes) == 0) { |
333 | $this->prepare(); |
334 | } |
335 | |
336 | foreach ($this->preparedRoutes as $prepRoute) { |
337 | if ($prepRoute['route'] == $route) { |
338 | $preparedRoute = $prepRoute; |
339 | break; |
340 | } |
341 | } |
342 | |
343 | if (!empty($params) && !empty($preparedRoute['params'])) { |
344 | foreach ($preparedRoute['params'] as $param) { |
345 | $paramName = $param['name']; |
346 | $paramString = null; |
347 | $paramUrlName = null; |
348 | |
349 | if (is_object($params) && isset($params->{$paramName})) { |
350 | if (is_array($params->{$paramName})) { |
351 | $paramString = implode('/', $params->{$paramName}); |
352 | $paramUrlName = $param['param'] . '*'; |
353 | } else { |
354 | $paramString = $params->{$paramName}; |
355 | $paramUrlName = $param['param']; |
356 | } |
357 | } else if (is_array($params) && isset($params[$paramName])) { |
358 | if (is_array($params[$paramName])) { |
359 | $paramString = implode('/', $params[$paramName]); |
360 | $paramUrlName = $param['param'] . '*'; |
361 | } else { |
362 | $paramString = $params[$paramName]; |
363 | $paramUrlName = $param['param']; |
364 | } |
365 | } |
366 | |
367 | $route = $baseUrl . str_replace($paramUrlName, '/' . $paramString, $route); |
368 | } |
369 | } |
370 | |
371 | $url = $route; |
372 | } |
373 | |
374 | return $url; |
375 | } |
376 | |
377 | } |