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.

Syntax$_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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · php
<?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.

Example · php
<?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.

Example · php
<?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

  • Be the first to comment on this lesson.