Skip to content

PHP Tutorial Practical PHP Development

Routing Concepts

Lesson 320 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

Routing means mapping a request (the URL path and the HTTP method) to the code that should handle it.

RequestHandler
GET /Home page
GET /productsProduct list
GET /products/42Show product 42
POST /productsCreate a product

A small router

index.php
PHP
<?php
final class Router
{
    /** @var array<int, array{method: string, regex: string, handler: callable}> */
    private array $routes = [];

    public function add(string $method, string $pattern, callable $handler): void
    {
        // "/products/{id}" becomes the regular expression  #^/products/(?<id>[^/]+)$#
        $regex = "#^" . preg_replace('#\{(\w+)\}#', '(?<$1>[^/]+)', $pattern) . "$#";

        $this->routes[] = ["method" => $method, "regex" => $regex, "handler" => $handler];
    }

    public function dispatch(string $method, string $path): array
    {
        $pathMatched = false;

        foreach ($this->routes as $route) {
            if (preg_match($route["regex"], $path, $matches) !== 1) {
                continue;
            }

            $pathMatched = true;

            if ($route["method"] !== $method) {
                continue;
            }

            $params = array_filter($matches, "is_string", ARRAY_FILTER_USE_KEY);

            return ($route["handler"])($params);
        }

        return $pathMatched
            ? ["status" => 405, "body" => "Method not allowed"]
            : ["status" => 404, "body" => "Not found"];
    }
}

$router = new Router();

$router->add("GET", "/", fn() => ["status" => 200, "body" => "Home"]);
$router->add("GET", "/products/{id}", fn(array $p) => ["status" => 200, "body" => "Product " . (int) $p["id"]]);
$router->add("POST", "/products", fn() => ["status" => 201, "body" => "Created"]);

foreach ([["GET", "/"], ["GET", "/products/42"], ["POST", "/products"], ["DELETE", "/products"], ["GET", "/missing"]] as [$method, $path]) {
    $r = $router->dispatch($method, $path);
    echo "$method $path -> {$r['status']} {$r['body']}\n";
}
Output
OUTPUT
GET / -> 200 Home
GET /products/42 -> 200 Product 42
POST /products -> 201 Created
DELETE /products -> 405 Method not allowed
GET /missing -> 404 Not found

How it works

  1. add() turns a pattern with {name} placeholders into a regular expression with named groups.
  2. dispatch() tests each route. If the path matches but the method does not, it remembers that.
  3. It passes the named parameters to the handler (["id" => "42"]).
  4. No match: 404. A path that exists with another method: 405.

Route parameters are strings from the URL

$p["id"] is the text "42", and it comes from the visitor. Validate it, cast it, and use a placeholder in SQL.

index.php
PHP
<?php
function intParam(array $params, string $name): ?int
{
    $v = filter_var($params[$name] ?? null, FILTER_VALIDATE_INT);

    return ($v === false || $v < 1) ? null : $v;
}

var_dump(intParam(["id" => "42"], "id"));
var_dump(intParam(["id" => "abc"], "id"));
var_dump(intParam(["id" => "-3"], "id"));
Output
OUTPUT
int(42)
NULL
NULL

Route groups and names

Real routers add groups (all routes under /admin need a login), named routes (generate a URL from a name) and middleware (code that runs before or after a handler). The idea is the same as above.

Do not trust the path

Normalise the path before matching (remove a trailing slash, ignore the query string with parse_url(..., PHP_URL_PATH)). Reject paths that contain .. if you ever map them to files.

Use a library when it grows

For a real project, use a tested router: FastRoute (a small, fast package), or the router that comes with Symfony or Laravel. Writing one is a great way to understand them, but production routers handle many edge cases (caching, optional segments, host matching).

terminal
BASH
composer require nikic/fast-route
info
Why this matters in Laravel
Laravel’s Route::get("/products/{id}", ...) is this idea with groups, middleware, model binding and a compiled route cache. The {id} placeholder works the same way.