0

Building a Request class from PHP superglobals ($_SERVER, $_GET, $_POST)

Advanced5 min read·fw-04-002

Concept

Building a Request class from PHP superglobals bridges PHP's raw request data ($_SERVER, $_GET, $_POST, $_FILES, $_COOKIE) into a structured object. This is the first step in handling any HTTP request in a framework.

$_SERVER key details:

  • HTTP_METHOD doesn't exist — the method is in $_SERVER['REQUEST_METHOD'].
  • $_SERVER['REQUEST_URI'] includes path + query string: /users/42?page=2.
  • $_SERVER['QUERY_STRING'] is just the query part: page=2.
  • $_SERVER['HTTP_*'] keys hold HTTP headers with HTTP_ prefix: HTTP_CONTENT_TYPEContent-Type.
  • $_SERVER['CONTENT_TYPE'] (no HTTP_ prefix) holds the Content-Type header.
  • $_SERVER['CONTENT_LENGTH'] holds the Content-Length header.

Parsing the body: For application/json requests, $_POST is empty. Read the raw body: file_get_contents('php://input').

Header normalization: HTTP_CONTENT_TYPE → strip HTTP_ prefix → replace _ with - → title-case → Content-Type. Do this for all HTTP_* server keys.

URI construction: Use parse_url($requestUri) to split path and query string separately. Or use PSR-7's UriInterface.

fromGlobals() factory method: The standard pattern — a static method that creates a Request from PHP's superglobals. In tests, create Requests directly with constructor args instead of from globals.

Code Example

php
<?php
namespace Framework\Http;

class Request
{
    private array $parsedBody;
    private array $headers;

    public function __construct(
        private readonly string $method,
        private readonly string $uri,
        private readonly string $queryString,
        private readonly array  $query,    // $_GET
        private readonly array  $post,     // $_POST
        private readonly array  $server,   // $_SERVER
        private readonly array  $cookies,  // $_COOKIE
        private readonly array  $files,    // $_FILES
        private readonly string $body,     // raw body
    ) {
        $this->headers    = $this->parseHeaders($server);
        $this->parsedBody = $this->parseBody();
    }

    public static function fromGlobals(): static
    {
        $rawBody = (string) file_get_contents('php://input');

        return new static(
            method:      $_SERVER['REQUEST_METHOD'] ?? 'GET',
            uri:         strtok($_SERVER['REQUEST_URI'] ?? '/', '?'),
            queryString: $_SERVER['QUERY_STRING'] ?? '',
            query:       $_GET,
            post:        $_POST,
            server:      $_SERVER,
            cookies:     $_COOKIE,
            files:       $_FILES,
            body:        $rawBody,
        );
    }

    public function method(): string { return strtoupper($this->method); }
    public function uri(): string    { return $this->uri; }
    public function query(string $key, mixed $default = null): mixed { return $this->query[$key] ?? $default; }
    public function input(string $key, mixed $default = null): mixed { return $this->parsedBody[$key] ?? $default; }
    public function header(string $name, string $default = ''): string { return $this->headers[strtolower($name)] ?? $default; }
    public function cookie(string $name): ?string { return $this->cookies[$name] ?? null; }
    public function server(string $key): ?string { return $this->server[$key] ?? null; }
    public function all(): array { return $this->parsedBody; }

    public function isJson(): bool
    {
        return str_contains($this->header('content-type'), 'application/json');
    }

    private function parseBody(): array
    {
        if ($this->isJson()) {
            return json_decode($this->body, true) ?? [];
        }
        return $this->post;
    }

    private function parseHeaders(array $server): array
    {
        $headers = [];
        foreach ($server as $key => $value) {
            if (str_starts_with($key, 'HTTP_')) {
                $name = strtolower(str_replace('_', '-', substr($key, 5)));
                $headers[$name] = $value;
            }
        }
        // Content-Type and Content-Length don't have HTTP_ prefix
        if (isset($server['CONTENT_TYPE']))   $headers['content-type']   = $server['CONTENT_TYPE'];
        if (isset($server['CONTENT_LENGTH'])) $headers['content-length'] = $server['CONTENT_LENGTH'];
        return $headers;
    }
}