A site built as products.php, basket.php and checkout.php carries the same fifteen lines at the top of every file: session start, config include, database connection, a login check. By the twentieth file three of the copies have drifted, and one of them starts the session after the first header() call. A front controller is the observation that those fifteen lines are the application and the rest is a lookup table.
<?php
// public/index.php — the only file the web server may reach
require __DIR__ . '/../bootstrap.php';
$routes = array(
'/products' => array('ProductController', 'index'),
'/basket' => array('BasketController', 'show'),
'/checkout' => array('CheckoutController', 'start'),
);
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if (!isset($routes[$path])) {
header('HTTP/1.1 404 Not Found');
require __DIR__ . '/../views/404.php';
exit;
}
list($class, $action) = $routes[$path];
echo call_user_func(array(new $class(), $action), $_GET);
What it costs is the convenience it replaces. A new URL is now an entry in a map rather than a file dropped in a directory, a typo produces a 404 instead of something visible in a directory listing, and nothing works at all until the web server has a rewrite rule pointing everything at index.php. What it buys is a single place where the session, the error handler and the authentication check are installed, so they cannot be missing from one page — which is the failure this replaces, and it is the kind nobody finds by testing. An exact-match array stays adequate for a long time. The point at which it grows a regex table and a parameter parser is the point at which an existing router costs less than the one you are about to finish writing.