A simple router and front controller. Instead of about.php, contact.php, and twenty copies of the same header, every request goes through public/index.php, which picks a handler based on the URL.
URL rewriting
Apache needs .htaccess in public/ to send requests to index.php (nginx uses try_files). Example for Apache:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
Parse the request path
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
// Optional: strip a base path if the app lives in a subdirectory
$uri = rtrim($uri, '/') ?: '/';
Route table
<?php
declare(strict_types=1);
namespace App;
class Router
{
private array $routes = [];
public function get(string $path, callable $handler): void
{
$this->routes['GET'][$path] = $handler;
}
public function post(string $path, callable $handler): void
{
$this->routes['POST'][$path] = $handler;
}
public function dispatch(string $method, string $uri): void
{
$handler = $this->routes[$method][$uri] ?? null;
if (!$handler) {
http_response_code(404);
echo 'Not found';
return;
}
$handler();
}
}
Wire it in index.php
$router = new AppRouter();
$router->get('/', fn () => (new AppControllersHomeController())->index());
$router->get('/notes', fn () => (new AppControllersNoteController())->index());
$router->post('/notes', fn () => (new AppControllersNoteController())->store());
$router->dispatch($method, $uri);
Route parameters (simple approach)
For /notes/5, match with a regex or split the path:
if (preg_match('#^/notes/(d+)$#', $uri, $m)) {
$id = (int) $m[1];
(new AppControllersNoteController())->show($id);
return;
}
Framework routers do fancier matching. For learning, explicit paths and one regex for IDs is enough. Keep the router readable.
404 and method not allowed
Return proper status codes. A missing page is 404. POST to a GET-only route can be 405. Browsers and APIs care about this.
The front controller pattern scales from a five-page site to most frameworks under the hood. You are building the same idea, stripped down.

