Building a Request class from PHP superglobals ($_SERVER, $_GET, $_POST)
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_METHODdoesn'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 withHTTP_prefix:HTTP_CONTENT_TYPE→Content-Type.$_SERVER['CONTENT_TYPE'](noHTTP_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
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;
}
}