How a Request Becomes a Page
A browser sends a request as plain text; the web server hands it to PHP, and whatever PHP prints becomes the response.
$_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI']The web is a conversation in plain text. A browser sends a request. A server sends back a response. Everything else is detail.
A request starts with one line: a method, a path and a protocol version, such as GET /articles/7?page=2 HTTP/1.1. Then come headers, one per line, each a name and a value. Then a blank line. Then an optional body, which is where a submitted form arrives.
Where PHP sits
PHP does not read that text itself. A web server such as nginx or Apache accepts the connection, parses the request and hands the parts to PHP. PHP puts them in arrays your script can read anywhere: $_SERVER for the method, path and headers, $_GET for the query string, $_POST for a form body. Anything the script echoes becomes the response body. header() adds a response header and http_response_code() sets the status.
The status code
The response's first line carries a three-digit code. 200 means here it is. 302 means look elsewhere. 404 means no such thing. 500 means the script failed. PHP sends 200 unless you say otherwise.
What trips people up
Headers go out ahead of the body, so they must be set before the first byte of output. A blank line above <?php is output. After that, header() fails with "headers already sent".
Example
When to use it
- Work out why a page shows "headers already sent" and where the stray output came from.
- Return 404 with a plain-text body when a requested record does not exist.
- Route every URL through a single index.php and load the right page file.
- Read the client's Host or Accept-Language header to decide what to serve.
More examples
The same request, as PHP receives it
The web server has already done the parsing the first example did by hand. Most request headers appear in $_SERVER as HTTP_ plus the header name uppercased with dashes turned into underscores, so Accept-Language becomes HTTP_ACCEPT_LANGUAGE. Content-Type and Content-Length are the exceptions: under Apache and under nginx with PHP-FPM they arrive as CONTENT_TYPE and CONTENT_LENGTH, with no HTTP_ prefix, which is why looking for $_SERVER['HTTP_CONTENT_TYPE'] finds nothing. These values come from the client, not from your server, so they are escaped before being printed like any other input. This does not run in the sandbox because there is no request to read.
<?php
declare(strict_types=1);
$method = $_SERVER['REQUEST_METHOD']; // 'GET'
$target = $_SERVER['REQUEST_URI']; // '/articles/7?page=2'
$path = parse_url($target, PHP_URL_PATH); // '/articles/7'
$host = $_SERVER['HTTP_HOST'] ?? ''; // the Host header
$agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; // the User-Agent header
echo htmlspecialchars("{$method} {$path} for {$host}", ENT_QUOTES, 'UTF-8');
echo '<br>';
echo htmlspecialchars($agent, ENT_QUOTES, 'UTF-8');Choosing the status code and the content type
A response is a status code, some headers, then the body. Content-Type tells the browser how to read the body: text/html renders as a page, application/json does not. Both header() calls happen before the first echo, because after that the headers are already on their way. Reach for this shape whenever a URL can legitimately have nothing to show.
<?php
declare(strict_types=1);
function findArticle(int $id): ?array
{
return $id === 7 ? ['id' => 7, 'title' => 'Hello'] : null;
}
$id = (int) ($_GET['id'] ?? 0);
$article = findArticle($id);
if ($article === null) {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo 'No such article';
exit;
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode($article, JSON_THROW_ON_ERROR);One file, many paths
Instead of one PHP file per URL, the web server sends every request to this one file and it decides what to load. That is a front controller, and it is what every framework does underneath. The method is part of the decision, because GET /contact shows the form and POST /contact handles it. Note that the file name comes from a fixed list, never from the path.
<?php
declare(strict_types=1);
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? '/';
$page = match ($method . ' ' . $path) {
'GET /' => 'home.php',
'GET /contact' => 'contact-form.php',
'POST /contact' => 'contact-save.php',
default => null,
};
if ($page === null) {
http_response_code(404);
echo 'Not found';
exit;
}
require __DIR__ . '/pages/' . $page;
Discussion