A front controller is the routing you already wrote by hand

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.