Skip to content

Commit 8a50a00

Browse files
committed
added multiple 404 routes
1 parent b6b7f5d commit 8a50a00

File tree

4 files changed

+145
-15
lines changed

4 files changed

+145
-15
lines changed

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,21 @@ $router->set404(function() {
289289
});
290290
```
291291

292+
You can also define multiple custom routes e.x. you want to define an `/api` route, you can print a custom 404 page:
293+
294+
```php
295+
$router->set404('/api(/.*)?', function() {
296+
header('HTTP/1.1 404 Not Found');
297+
header('Content-Type: application/json');
298+
299+
$jsonArray = array();
300+
$jsonArray['status'] = "404";
301+
$jsonArray['status_text'] = "route not defined";
302+
303+
echo json_encode($jsonArray);
304+
});
305+
```
306+
292307
Also supported are `Class@Method` callables:
293308

294309
```php

demo/index.php

+36-1
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,49 @@
1919
echo '404, route not found!';
2020
});
2121

22+
// custom 404
23+
$router->set404('/test(/.*)?', function () {
24+
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
25+
echo '<h1><mark>404, route not found!</mark></h1>';
26+
});
27+
28+
$router->set404('/api(/.*)?', function() {
29+
header('HTTP/1.1 404 Not Found');
30+
header('Content-Type: application/json');
31+
32+
$jsonArray = array();
33+
$jsonArray['status'] = "404";
34+
$jsonArray['status_text'] = "route not defined";
35+
36+
echo json_encode($jsonArray);
37+
});
38+
2239
// Before Router Middleware
2340
$router->before('GET', '/.*', function () {
2441
header('X-Powered-By: bramus/router');
2542
});
2643

2744
// Static route: / (homepage)
2845
$router->get('/', function () {
29-
echo '<h1>bramus/router</h1><p>Try these routes:<p><ul><li>/hello/<em>name</em></li><li>/blog</li><li>/blog/<em>year</em></li><li>/blog/<em>year</em>/<em>month</em></li><li>/blog/<em>year</em>/<em>month</em>/<em>day</em></li><li>/movies</li><li>/movies/<em>id</em></li></ul>';
46+
echo '<h1>bramus/router</h1>
47+
<p>Try these routes:<p>
48+
<ul>
49+
<li><a href="/hello/joe">/hello/<em>name</em></a></li>
50+
<li><a href="/blog">/blog</a></li>
51+
<li><a href="/blog/'.date('Y').'">/blog/<em>year</em></a></li>
52+
<li><a href="/blog/'.date('Y').'/'.date('m').'">/blog/<em>year</em>/<em>month</em></a></li>
53+
<li><a href="/blog/'.date('Y').'/'.date('m').'/'.date('d').'">/blog/<em>year</em>/<em>month</em>/<em>day</em></a></li>
54+
<li><a href="/movies">/movies</a></li>
55+
<li><a href="/movies/23">/movies/<em>id</em></a></li>
56+
</ul>
57+
<br><br>
58+
<p>Custom error routes</p>
59+
<ul>
60+
<li><a href="/something">/*</a> <em>Normal 404</em></li>
61+
<li><a href="/test">/test/*</a> <em>Custom 404</em></li>
62+
<li><a href="/api/getUser">/api/getUser</a> <em>API 404</em></li>
63+
</ul>
64+
';
3065
});
3166

3267
// Static route: /hello

src/Bramus/Router/Router.php

+84-14
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ class Router
2323
private $beforeRoutes = array();
2424

2525
/**
26-
* @var object|callable The function to be executed when no route has been matched
26+
* @var array [object|callable] The function to be executed when no route has been matched
2727
*/
28-
protected $notFoundCallback;
28+
protected $notFoundCallback = [];
2929

3030
/**
3131
* @var string Current base route, used for (sub)route mounting
@@ -289,7 +289,7 @@ public function run($callback = null)
289289

290290
// If no route was handled, trigger the 404 (if any)
291291
if ($numHandled === 0) {
292-
$this->trigger404();
292+
$this->trigger404($this->afterRoutes[$this->requestedMethod]);
293293
} // If a route was handled, perform the finish callback (if any)
294294
else {
295295
if ($callback && is_callable($callback)) {
@@ -309,24 +309,92 @@ public function run($callback = null)
309309
/**
310310
* Set the 404 handling function.
311311
*
312+
* @param object|callable|string $match_fn The function to be executed
312313
* @param object|callable $fn The function to be executed
313314
*/
314-
public function set404($fn)
315+
public function set404($match_fn, $fn = null)
315316
{
316-
$this->notFoundCallback = $fn;
317+
if (!is_null($fn)) {
318+
$this->notFoundCallback[$match_fn] = $fn;
319+
} else {
320+
$this->notFoundCallback['/'] = $match_fn;
321+
}
317322
}
318323

319324
/**
320325
* Triggers 404 response
326+
*
327+
* @param string $pattern A route pattern such as /about/system
321328
*/
322-
public function trigger404(){
323-
if ($this->notFoundCallback) {
324-
$this->invoke($this->notFoundCallback);
325-
} else {
326-
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
329+
public function trigger404($match = null){
330+
331+
// Counter to keep track of the number of routes we've handled
332+
$numHandled = 0;
333+
334+
// handle 404 pattern
335+
if (count($this->notFoundCallback) > 0)
336+
{
337+
// loop fallback-routes
338+
foreach ($this->notFoundCallback as $route_pattern => $route_callable) {
339+
340+
// matches result
341+
$matches = [];
342+
343+
// check if there is a match and get matches as $matches (pointer)
344+
$is_match = $this->patternMatches($route_pattern, $this->getCurrentUri(), $matches, PREG_OFFSET_CAPTURE);
345+
346+
// is fallback route match?
347+
if ($is_match) {
348+
349+
// Rework matches to only contain the matches, not the orig string
350+
$matches = array_slice($matches, 1);
351+
352+
// Extract the matched URL parameters (and only the parameters)
353+
$params = array_map(function ($match, $index) use ($matches) {
354+
355+
// We have a following parameter: take the substring from the current param position until the next one's position (thank you PREG_OFFSET_CAPTURE)
356+
if (isset($matches[$index + 1]) && isset($matches[$index + 1][0]) && is_array($matches[$index + 1][0])) {
357+
if ($matches[$index + 1][0][1] > -1) {
358+
return trim(substr($match[0][0], 0, $matches[$index + 1][0][1] - $match[0][1]), '/');
359+
}
360+
} // We have no following parameters: return the whole lot
361+
362+
return isset($match[0][0]) && $match[0][1] != -1 ? trim($match[0][0], '/') : null;
363+
}, $matches, array_keys($matches));
364+
365+
$this->invoke($route_callable);
366+
367+
++$numHandled;
368+
}
369+
}
370+
if($numHandled == 0 and $this->notFoundCallback['/']) {
371+
$this->invoke($this->notFoundCallback['/']);
372+
} elseif ($numHandled == 0) {
373+
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
374+
}
327375
}
328376
}
329377

378+
/**
379+
* Replace all curly braces matches {} into word patterns (like Laravel)
380+
* Checks if there is a routing match
381+
*
382+
* @param $pattern
383+
* @param $uri
384+
* @param $matches
385+
* @param $flags
386+
*
387+
* @return bool -> is match yes/no
388+
*/
389+
private function patternMatches($pattern, $uri, &$matches, $flags)
390+
{
391+
// Replace all curly braces matches {} into word patterns (like Laravel)
392+
$pattern = preg_replace('/\/{(.*?)}/', '/(.*?)', $pattern);
393+
394+
// we may have a match!
395+
return boolval(preg_match_all('#^' . $pattern . '$#', $uri, $matches, PREG_OFFSET_CAPTURE));
396+
}
397+
330398
/**
331399
* Handle a a set of routes: if a match is found, execute the relating handling function.
332400
*
@@ -345,11 +413,13 @@ private function handle($routes, $quitAfterRun = false)
345413

346414
// Loop all routes
347415
foreach ($routes as $route) {
348-
// Replace all curly braces matches {} into word patterns (like Laravel)
349-
$route['pattern'] = preg_replace('/\/{(.*?)}/', '/(.*?)', $route['pattern']);
350416

351-
// we have a match!
352-
if (preg_match_all('#^' . $route['pattern'] . '$#', $uri, $matches, PREG_OFFSET_CAPTURE)) {
417+
// get routing matches
418+
$is_match = $this->patternMatches($route['pattern'], $uri, $matches, PREG_OFFSET_CAPTURE);
419+
420+
// is there a valid match?
421+
if ($is_match) {
422+
353423
// Rework matches to only contain the matches, not the orig string
354424
$matches = array_slice($matches, 1);
355425

tests/RouterTest.php

+10
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,10 @@ public function test404()
628628
echo 'route not found';
629629
});
630630

631+
$router->set404('/api(/.*)?', function () {
632+
echo 'api route not found';
633+
});
634+
631635
// Test the /hello route
632636
ob_start();
633637
$_SERVER['REQUEST_URI'] = '/';
@@ -640,6 +644,12 @@ public function test404()
640644
$router->run();
641645
$this->assertEquals('route not found', ob_get_contents());
642646

647+
// Test the custom api 404
648+
ob_clean();
649+
$_SERVER['REQUEST_URI'] = '/api/getUser';
650+
$router->run();
651+
$this->assertEquals('api route not found', ob_get_contents());
652+
643653
// Cleanup
644654
ob_end_clean();
645655
}

0 commit comments

Comments
 (0)