Build a Simple PHP Router from Scratch: Step‑by‑Step Guide

This tutorial walks you through creating a lightweight PHP router—from understanding routing fundamentals and setting up project structure to writing the core Router class, configuring URL rewriting, defining routes with parameters, testing, and exploring extensions—empowering you to grasp framework internals and handle HTTP requests without a full‑stack framework.

php Courses
php Courses
php Courses
Build a Simple PHP Router from Scratch: Step‑by‑Step Guide

In modern PHP web development, frameworks like Laravel or Symfony provide powerful routers that map HTTP requests to controllers. Understanding how a router works deepens your knowledge of these frameworks and lets you build lightweight projects without a full‑stack solution.

We will build a simple, functional PHP router from scratch.

1. What is a router?

A router is the traffic controller of an application. Its main tasks are:

Parse request: obtain the current request URI (e.g., /about or /contact) and method (GET, POST, etc.).

Match route: find a predefined route rule that matches the URI and method.

Execute action: invoke the associated handler, which can be a closure, a controller method, or a view file.

2. Project structure and preparation

Create a clean project directory using the Front Controller pattern, where all requests go through index.php.

Project layout:

/my_simple_router
│ .htaccess          # URL rewrite (Apache)
│ index.php          # Front controller, entry point
└───src/
        Router.php   # Core router class

Environment requirements: a running Apache server with mod_rewrite enabled (or Nginx) and PHP support.

3. Implementation steps

Step 1: Configure URL rewriting (.htaccess)

Create a .htaccess file in the project root to route all requests to index.php:

RewriteEngine On

# If the request is not an existing file or directory
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

# Rewrite to index.php
RewriteRule ^(.*)$ index.php [QSA,L]

This configuration forwards non‑existent URLs to index.php for routing.

Step 2: Create the core router class (src/Router.php)

The class stores route definitions and performs matching.

<?php
class Router
{
    // Store all registered routes
    private $routes = [];

    // Register a GET route
    public function get($path, $callback)
    {
        $this->addRoute('GET', $path, $callback);
    }

    // Register a POST route
    public function post($path, $callback)
    {
        $this->addRoute('POST', $path, $callback);
    }

    // Generic method to add a route
    private function addRoute($method, $path, $callback)
    {
        $this->routes[] = [
            'method'   => $method,
            'path'     => $path,
            'callback' => $callback
        ];
    }

    // Dispatch the current request – core matching logic
    public function dispatch()
    {
        // Get current request URI and method
        $requestUri    = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
        $requestMethod = $_SERVER['REQUEST_METHOD'];

        // Remove base path if the project is not in the web root
        $basePath = '/my_simple_router'; // adjust or set to '' if in root
        if ($basePath && strpos($requestUri, $basePath) === 0) {
            $requestUri = substr($requestUri, strlen($basePath));
        }

        // Iterate over registered routes and try to match
        foreach ($this->routes as $route) {
            // Convert route path to a regex, supporting dynamic parameters like /user/{id}
            $pattern = '#^' . preg_replace('/\{([a-zA-Z0-9_]+)\}/', '([a-zA-Z0-9_]+)', $route['path']) . '$#';

            // Check method and path match
            if ($route['method'] === $requestMethod && preg_match($pattern, $requestUri, $matches)) {
                // Remove the full match, keep captured parameters
                array_shift($matches);

                // Call the callback
                if (is_callable($route['callback'])) {
                    call_user_func_array($route['callback'], $matches);
                } else {
                    // Could be expanded to handle controller strings like 'HomeController@index'
                    echo "Callback is not callable.";
                }
                return; // stop after a successful match
            }
        }

        // No matching route – return 404
        $this->sendNotFound();
    }

    // Send a 404 response
    private function sendNotFound()
    {
        header("HTTP/1.0 404 Not Found");
        echo '404 - Page not found.';
        exit;
    }
}

Code explanation:

$routes

: array that stores all defined routes. get() and post(): convenience methods for registering routes for specific HTTP methods. addRoute(): stores method, path, and callback in $routes. dispatch(): core method that obtains the current URI and method, converts each route path to a regex (supporting dynamic segments like {id}), matches them, and invokes the corresponding callback with captured parameters; if none match, it sends a 404.

Step 3: Create the front controller (index.php)

Use the router in index.php:

<?php
require_once 'src/Router.php';

$router = new Router();

// 1. Define basic routes
$router->get('/', function () {
    echo "<h1>Welcome to the Homepage!</h1>";
});

$router->get('/about', function () {
    echo "<h1>About Us</h1><p>This is a simple PHP router.</p>";
});

// 2. Define a route with a parameter (e.g., /user/123)
$router->get('/user/{id}', function ($id) {
    echo "<h1>User Profile</h1><p>Showing profile for User ID: $id</p>";
});

// 3. Define a POST route (e.g., login form handling)
$router->post('/login', function () {
    $username = $_POST['username'] ?? 'Unknown';
    echo "<p>Thank you for logging in, $username!</p>";
});

// ... more routes can be added here

// Finally, let the router handle the current request
$router->dispatch();

4. Test your router

Homepage: visit http://localhost/my_simple_router/ – you should see “Welcome to the Homepage!”.

About page: visit http://localhost/my_simple_router/about – displays the about page.

Dynamic route: visit http://localhost/my_simple_router/user/456 – shows “Showing profile for User ID: 456”.

POST request: create a simple HTML form with action set to /my_simple_router/login and method set to post; submitting should display the greeting.

404 page: visit an undefined path such as http://localhost/my_simple_router/unknown – you will see a 404 error.

5. Extension ideas

Support more HTTP methods: add put(), patch(), delete(), etc.

Controller support: modify dispatch to resolve strings like 'HomeController@index' and automatically instantiate the controller.

Complex route patterns: allow regex constraints on parameters (e.g., {id:\d+}).

Middleware: plug in logic before or after route execution for authentication, logging, etc.

Named routes: assign names to routes for easier URL generation.

Conclusion

Through this exercise we built the core engine of a PHP router. You learned how to parse URIs, match routes, handle dynamic parameters, and gain insight into the inner workings of modern PHP frameworks. This lightweight router is sufficient for many small projects, and understanding these fundamentals makes advanced framework routing documentation much more approachable.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentHTTPRouterPHPTutorial
php Courses
Written by

php Courses

php中文网's platform for the latest courses and technical articles, helping PHP learners advance quickly.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.