The request/response model and the built-in server
A browser sends an HTTP request: a method (GET, POST…), a path (/products?page=2), headers and sometimes a body. The web server (nginx or Apache) hands PHP requests for .php files, usually through PHP-FPM. PHP runs the script from the top, everything it echoes becomes the response body, and header() calls add response headers. Then the script ends and everything is thrown away — variables, objects, open connections. The next request starts from zero. This "shared-nothing" model is why PHP apps are easy to scale and why state that must survive between requests lives in a session, a cookie or a database.
For development you do not need nginx: PHP has a built-in web server (introduced in Module 00). It serves the current directory and runs .php files. It handles one request at a time and is not for production.
mkdir hello-web && cd hello-web
# create index.php (below), then:
php -S localhost:8000
# open http://localhost:8000/?name=Asha in the browser
# route every request through one file (a front controller):
php -S localhost:8000 index.php<?php
declare(strict_types=1);
$name = $_GET['name'] ?? 'world';
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
header('Content-Type: text/html; charset=utf-8');
?>
<!doctype html>
<title>Hello</title>
<h1>Hello, <?= htmlspecialchars($name) ?>!</h1>
<p>You made a <?= $method ?> request for <code><?= htmlspecialchars($path) ?></code>.</p>Text outside <?php ?> is sent as-is; <?= ?> echoes a value. Visiting /?name=Asha renders "Hello, Asha!".
Because a request is just input and the page is just output, you can model the whole cycle as a function — which is exactly how frameworks and their tests see it. The examples in this module run on the command line by building the input arrays themselves.
<?php
declare(strict_types=1);
/** @return array{int, array<string,string>, string} status, headers, body */
function handle(string $method, string $uri): array
{
$path = parse_url($uri, PHP_URL_PATH);
parse_str(parse_url($uri, PHP_URL_QUERY) ?? '', $query);
if ($method === 'GET' && $path === '/hello') {
$name = htmlspecialchars($query['name'] ?? 'world');
return [200, ['Content-Type' => 'text/html'], "<h1>Hello, $name!</h1>"];
}
return [404, ['Content-Type' => 'text/plain'], 'Not Found'];
}
foreach ([['GET', '/hello?name=Asha'], ['GET', '/hello'], ['POST', '/hello'], ['GET', '/admin']] as [$m, $u]) {
[$status, $headers, $body] = handle($m, $u);
echo "$m $u -> $status {$headers['Content-Type']} | $body\n";
}GET /hello?name=Asha -> 200 text/html | <h1>Hello, Asha!</h1>
GET /hello -> 200 text/html | <h1>Hello, world!</h1>
POST /hello -> 404 text/plain | Not Found
GET /admin -> 404 text/plain | Not Foundparse_url() splits the URI; parse_str() turns a query string into an array — the same thing PHP does to fill $_GET.
