One Branch per Route
Routing decides which code answers which request. PHP has no built-in route table — the router script reads the request itself: $_SERVER["REQUEST_URI"] holds the path, and exploding it on / gives the segments.
A segment that is data — the 7 in /users/7 — is a path parameter: instead of matching it literally, the router captures it and hands it to the handler. Frameworks add conveniences on top, but this is the mechanism underneath.
Routes and Path Values
Two routes: the list at /users and one user at /users/{id}. The second segment of the path is the path parameter — the piece of the URL that is data.
A route with a path parameter
The same router script sends /users to one branch and /users/7 to another, and the second path segment hands over the 7 as data.
Output: PHP script
<?php
// The router script: it explodes the path and picks a branch.
$router = <<<'PHP'
<?php
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$segments = explode("/", trim($path, "/"));
if ($path === "/users") {
echo "all users: Ana, Ben, Cleo\n";
} elseif (count($segments) === 2 && $segments[0] === "users") {
echo "one user, id = {$segments[1]}\n";
} else {
http_response_code(404);
echo "not found\n";
}
PHP;
$routerFile = sys_get_temp_dir() . "/pc-path-values.php";
file_put_contents($routerFile, $router);
// A free port, then PHP's built-in web server as a child process.
$probe = stream_socket_server("tcp://127.0.0.1:0");
$port = (int) explode(":", stream_socket_get_name($probe, false))[1];
fclose($probe);
$server = proc_open(["php", "-S", "127.0.0.1:$port", $routerFile],
[1 => ["file", "/dev/null", "w"], 2 => ["file", "/dev/null", "w"]], $pipes);
do {
usleep(20000);
$up = @fsockopen("127.0.0.1", $port);
} while ($up === false);
fclose($up);
$context = stream_context_create(["http" => ["ignore_errors" => true]]);
foreach (["/users", "/users/7"] as $path) {
$body = file_get_contents("http://127.0.0.1:$port$path", false, $context);
echo "GET $path -> $body";
}
proc_terminate($server);
proc_close($server);
unlink($routerFile);
Query Parameters
The part after ? holds query parameters — options for the request, like a search text or a limit: /search?q=php&limit=2. PHP parses them for you into the $_GET array, always as strings, with ?? supplying a default for missing ones.
The rule of thumb: the path says which resource, the query says how — filters, sorting, paging.
Reading the query string
The handler reads q and limit from $_GET. The second request skips limit, so the handler falls back to a default — missing parameters are normal, plan for them.
Output: PHP script
<?php
// Query parameters arrive parsed in $_GET, always as strings.
$router = <<<'PHP'
<?php
$q = $_GET["q"] ?? "";
$limit = $_GET["limit"] ?? "10";
echo "searching for $q with limit $limit\n";
PHP;
$routerFile = sys_get_temp_dir() . "/pc-query-params.php";
file_put_contents($routerFile, $router);
// A free port, then PHP's built-in web server as a child process.
$probe = stream_socket_server("tcp://127.0.0.1:0");
$port = (int) explode(":", stream_socket_get_name($probe, false))[1];
fclose($probe);
$server = proc_open(["php", "-S", "127.0.0.1:$port", $routerFile],
[1 => ["file", "/dev/null", "w"], 2 => ["file", "/dev/null", "w"]], $pipes);
do {
usleep(20000);
$up = @fsockopen("127.0.0.1", $port);
} while ($up === false);
fclose($up);
$context = stream_context_create(["http" => ["ignore_errors" => true]]);
foreach (["/search?q=php&limit=2", "/search?q=json"] as $path) {
$body = file_get_contents("http://127.0.0.1:$port$path", false, $context);
echo "GET $path -> $body";
}
proc_terminate($server);
proc_close($server);
unlink($routerFile);