Lesson 11 / 14
Superglobals
How request data reaches PHP through superglobals: a missing key heard through the warning channel, values that are always strings, and the client choosing the type of a value.
Published Updated
In this lesson
- Measures the channel through which a missing query variable is heard
- Verifies with the built-in server that request data reaches PHP as a string or an array
- Explains that the client can choose the type of a value and that the status code is not a reliable measure of the channel
Contents
The String Handling lesson measured that json_decode returns null once malformed data is decoded, and that the return channel cannot tell this null apart from a valid null value. Telling them apart was left to the caller, who had to ask an extra question. Up to that lesson all of the data lived inside the program: array literals, string literals, one JSON text. In this lesson the data arrives from outside, and the question stays the same: what is heard when request data arrives missing or in an unexpected form?
M04 established the structure of an HTTP request and M16/K01 the server-side life cycle of a request; here only the mechanism PHP uses to hand over this data is measured. The mechanism is called a superglobal: a predefined array that PHP fills at the start of the request and that is reachable from every scope without being declared. Four of them matter for request data: $_GET carries the query string, $_POST the form fields in the request body, $_SERVER request information such as the method, and $_FILES the uploaded files. The last one is the subject of the third lesson.
A Missing Key Raises a Warning
The first measurement runs on the command line. There is no request there and $_GET is empty; reading a key that does not exist behaves the same as asking for a variable that is absent from a query string. The measurement uses channel.php, the core of the course, which is given verbatim in this lesson as well.
<?php // channel.php — helpers that classify which channel a problem is heard through // Level names of caught warnings collect here; the message text is not kept. $GLOBALS['heard_warnings'] = []; set_error_handler(function (int $level): bool { $names = [E_WARNING => 'E_WARNING', E_NOTICE => 'E_NOTICE', E_DEPRECATED => 'E_DEPRECATED']; $GLOBALS['heard_warnings'][] = $names[$level] ?? 'E_' . $level; return true; }); // Turns a value into a short text together with its type. function show(mixed $value): string { return match (true) { $value === null => 'null', is_bool($value) => $value ? 'true' : 'false', is_array($value) => 'array(' . count($value) . ')', default => get_debug_type($value) . ' ' . $value, }; } // Runs the job and asks, in order, which channel the problem is heard through: // first a throw, then a warning, then a false/null return; if none of them, silent. function listen(callable $job): array { $GLOBALS['heard_warnings'] = []; try { $result = $job(); } catch (Throwable $error) { return ['halts', get_class($error)]; } if ($GLOBALS['heard_warnings']) { $levels = implode(',', array_unique($GLOBALS['heard_warnings'])); return ['warning', $levels . ', result ' . show($result)]; } if ($result === false || $result === null) { return ['return', 'result ' . show($result)]; } return ['silent', 'result ' . show($result)]; } // Runs the code in a separate PHP execution; exit code 255 means execution halted. function run_separately(string $code): array { exec('php -d display_errors=0 -r ' . escapeshellarg($code) . ' 2>&1', $lines, $exit); $channel = $exit === 255 ? 'halts' : 'silent'; return [$channel, 'lines out ' . count($lines) . ', exit ' . $exit]; }
<?php // missing.php — which channel is heard when reading from an empty $_GET require 'channel.php'; $measurements = [ 'missing query variable' => fn() => $_GET['page'], 'null coalescing' => fn() => $_GET['page'] ?? 1, 'filter_input' => fn() => filter_input(INPUT_GET, 'page'), ]; foreach ($measurements as $name => $measurement) { [$channel, $evidence] = listen($measurement); printf("%-24s%-10s%s\n", $name, $channel, $evidence); }
missing query variable warning E_WARNING, result null null coalescing silent result int 1 filter_input return result null
The first row is the core observation missing query variable: the result is null, but an E_WARNING was reported and execution continued. This is the warning channel. The caller did not have to ask anything; the channel spoke on its own. The read happened inside a function, without a global declaration, which is exactly what defines a superglobal. RR1. $_GET is empty in this measurement because execution is on the command line; the channel of the row follows from the key’s absence in the array and does not depend on the source of the request.
The second row is the opposite channel. The null coalescing operator removes the warning, and the same missing key is heard through the silent channel; it is the request-data counterpart of the measurement in the Operators lesson. Meeting a missing key with a default is a deliberate choice, and this is the right tool for it, but no channel reports the absence. The third row is filter_input: on the command line there is no request, so it returns null and no signal is raised; the problem is heard only by a caller that looks at the return value. This is the return channel, and the functions of the next lesson use the same channel.
Data Arriving at the Server
For a real request, the built-in server that ships with PHP is used: the php -S command starts a server at the given address and executes the requested script from the beginning for each request. The measurement has two parts: a script that prints the request data, and a shell script that starts the server, sends the requests and then shuts it down. RR2. Every php -S call is started with display_errors off; with the setting off, an uncaught error produces status code 500, and no error text falls into the lesson output.
<?php // request.php — prints the type of the incoming data and the request method foreach ($_GET as $name => $value) { $length = is_array($value) ? count($value) : strlen($value); echo 'get ', $name, '=', get_debug_type($value), ' ', $length, "\n"; } foreach ($_POST as $name => $value) { echo 'post ', $name, '=', get_debug_type($value), ' ', strlen($value), "\n"; } echo 'method=', $_SERVER['REQUEST_METHOD'], "\n";
# server.sh — starts the built-in server, sends the requests, shuts it down php -S 127.0.0.1:8081 -d display_errors=0 >/dev/null 2>&1 & pid=$! for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8081/request.php && break; sleep 0.1; done echo '--- GET ?n=5&b=' curl -sg -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/request.php?n=5&b=' echo '--- GET ?a[]=1&a[]=2' curl -sg -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/request.php?a[]=1&a[]=2' echo '--- POST body x=1, query n=5' curl -sg -d 'x=1' -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/request.php?n=5' kill $pid
--- GET ?n=5&b= get n=string 1 get b=string 0 method=GET status=200 --- GET ?a[]=1&a[]=2 get a=array 2 method=GET status=200 --- POST body x=1, query n=5 get n=string 1 post x=string 1 method=POST status=200
Three results hold throughout this lesson. First, every value is a string: the 5 that arrives with ?n=5 is not an int but a string of length 1, and the empty value that arrives with ?b= is not null or false but a string of length 0. The request travels as text and PHP makes no type decision; when a number is expected, converting the string is the code’s job. Second, appending square brackets to the name (a[]) turns the value into an array: when the same name arrives twice it becomes an array, and the element count shows 2. Third, in a POST request the query string still lands in $_GET, the field in the body goes to $_POST, and REQUEST_METHOD reports the method as POST; so that the two sources do not get mixed up, the code states explicitly which one it reads from.
RR3. What gets printed is not the values themselves but their type and length, which keeps the output independent of the environment. The status code is 200 because request.php raised no error.
Headers and the Raw Query String Live in $_SERVER
The parsed form of the query string is in $_GET; the raw form and the request headers are in the $_SERVER array. The QUERY_STRING key carries the unparsed text, and keys that begin with the HTTP_ prefix carry the headers, uppercased and with hyphens converted to underscores. The client chooses the header, so it may be absent, and reading a missing header uses the same channel as a missing query variable.
<?php // header.php — the raw query string and a custom request header echo 'method=', $_SERVER['REQUEST_METHOD'], "\n"; echo 'query=', $_SERVER['QUERY_STRING'], "\n"; echo 'header=', $_SERVER['HTTP_X_CLIENT'] ?? 'none', "\n"; echo 'parsed=', count($_GET), "\n";
# header.sh — sends two requests, one with the header and one without php -S 127.0.0.1:8081 -d display_errors=0 >/dev/null 2>&1 & pid=$! for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8081/header.php && break; sleep 0.1; done echo '--- with header' curl -s -H 'X-Client: test' 'http://127.0.0.1:8081/header.php?a=1&b=2&a=3' echo '--- without header' curl -s 'http://127.0.0.1:8081/header.php' kill $pid
--- with header method=GET query=a=1&b=2&a=3 header=test parsed=2 --- without header method=GET query= header=none parsed=0
In the raw string a appears twice, but $_GET counts only two keys: a repeated name collapses to a single key holding the last value. The header appears under the HTTP_X_CLIENT key when the client sends it; when it does not, the key is absent and the null coalescing operator supplies the default. RR4. The header value is in the client’s hands, so it is not a trustworthy source; what this lesson measures is through which channel the arrival or absence of the header is heard, and the null coalescing operator used here moves it to the silent channel.
An Empty Value Gets Three Results from Three Tests
Because request data arrives as strings, the question “is there a value” can be asked with three different operations: isset, empty and the null coalescing operator. None of the three raises a warning for a missing key, so all three belong to the silent channel. The difference shows up in what the problem is: the 0 that arrives with ?n=0 and the empty string that arrives with ?b= are two cases in which the key exists but the value looks empty.
<?php // blank.php — the result of three tests on values that exist but look empty foreach (['n', 'b', 'none'] as $name) { $present = isset($_GET[$name]) ? 'yes' : 'no'; $blank = empty($_GET[$name]) ? 'yes' : 'no'; $fallback = $_GET[$name] ?? 'default'; printf("%-5s isset=%-6s empty=%-6s coalesce=[%s]\n", $name, $present, $blank, $fallback); }
# blank.sh — calls blank.php with a zero, an empty string and a missing key php -S 127.0.0.1:8081 -d display_errors=0 >/dev/null 2>&1 & pid=$! for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8081/blank.php && break; sleep 0.1; done curl -s -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/blank.php?n=0&b=' kill $pid
n isset=yes empty=yes coalesce=[0] b isset=yes empty=yes coalesce=[] none isset=no empty=yes coalesce=[default] status=200
isset says yes for both existing keys and no for the missing one. empty treats both the string 0 and the empty string as empty; even though the value of n is a valid number, empty classifies it as “no value”. The null coalescing operator keeps the empty string and falls to the default only for a missing key. RR5. The three tests make three different decisions on the same input and none of them produces a signal: code that picks the wrong test silently counts a valid 0 as missing. This is the request-data counterpart of the not operator problem on the return channel (measured in the Form Handling lesson).
The Merged Array Depends on an ini Value
The $_REQUEST array collects the contents of $_GET and $_POST, and depending on the setting $_COOKIE as well, in a single array. When the same name exists in both sources, the request_order ini value decides which one wins. The lesson trusts no ini value, so the setting is passed with -d and both orders are measured.
<?php // merged.php — what $_REQUEST gives when the same name arrives in both the query string and the body echo 'get=', $_GET['x'], ' post=', $_POST['x'], ' request=', $_REQUEST['x'], "\n";
# merged.sh — tries two different orders of request_order for order in GP PG; do php -S 127.0.0.1:8081 -d display_errors=0 -d request_order=$order >/dev/null 2>&1 & pid=$! for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8081/merged.php && break; sleep 0.1; done echo "--- request_order=$order" curl -s -d 'x=body' 'http://127.0.0.1:8081/merged.php?x=query' kill $pid wait $pid 2>/dev/null done true
--- request_order=GP get=query post=body request=body --- request_order=PG get=query post=body request=query
$_GET and $_POST give the same values under both orders, but $_REQUEST changes: with the order GP the value in the body wins, with PG the value in the query string wins. RR6. The same request reads different data depending on the setting’s value, and no channel announces it. So if the code cares about which source the data came from, it reads explicitly from either $_GET or $_POST.
The Bounding Measurement
The second finding in the Data Arriving at the Server section raises a problem: the client can send an array instead of a string for any name. If the script expects a string and the client sends an array with ?u[]=x, strlen throws a TypeError on that array argument. This is an uncaught error and belongs to the halts channel. The author of the code assumed a missing key would be heard through the warning channel, yet the client chose the channel. The channel of a piece of code is never bound to the code alone, because the client determines the input.
How the same error appears depends on whether it arrives before or after the output. Two scripts make the same strlen call; one makes the call before any output, the other after an echo.
<?php // early.php — the error arrives before any output starts $u = strlen($_GET['u']); echo "length=$u\n";
<?php // late.php — the same error arrives after an echo echo "started\n"; $u = strlen($_GET['u']); echo "length=$u\n";
# type.sh — the status code the same array input gives in two scripts php -S 127.0.0.1:8081 -d display_errors=0 >/dev/null 2>&1 & pid=$! for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8081/early.php && break; sleep 0.1; done echo '--- early.php string' curl -sg -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/early.php?u=abc' echo '--- early.php array' curl -sg -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/early.php?u[]=x' echo '--- late.php array' curl -sg -w 'status=%{http_code}\n' 'http://127.0.0.1:8081/late.php?u[]=x' kill $pid
--- early.php string length=3 status=200 --- early.php array status=500 --- late.php array started status=200
When a string is sent, everything works. When an array is sent, early.php prints nothing and returns 500; late.php has already printed the started line and sent the headers, so it returns 200 with a partial body. Same request, same error class, two different status codes. RR7. If the error arrives before the response body starts, the server has not yet sent the headers and can produce a 500; once the headers have gone, the status code cannot change. The status codes established in M04 report the result of a request, but here what the code tells is where in the execution the error arose, not whether there was an error. Hence the status code is not a reliable measure of the channel: a client that sees 200 cannot know that an uncaught error occurred on the server.
Summary
- Superglobals (
$_GET,$_POST,$_SERVER,$_FILES) are reachable from every scope without being declared, and they are the mechanism through which PHP hands over request data. - The channel measured in this lesson is warning: reading a key that does not exist from an empty
$_GETgivesnulland reports anE_WARNING; execution continues. - The null coalescing operator moves the same absence to the silent channel;
filter_inputon the command line returnsnulland so announces it through the return channel. - Request data arrives as
string, a name in the forma[]makes it anarray; in a POST request the query string lands in$_GETand the body in$_POST. - Because the client can choose the type, the channel is in the client’s hands as well: sending an array makes
strlenthrow aTypeError, and the status code is 500 or 200 depending on whether the error arrives before or after the output.
Next Step
In this lesson the key was either missing or arrived with the type the client chose, but the content of the value was never questioned. Who decides whether a value that arrives as a string is a number? The Form Handling lesson measures how an invalid value such as 12a is filtered with filter_var and with which return value the function reports the failure; whether a valid 0 gets confused with that same value is also that lesson’s question.
To keep your progress and take notes, Log in
My notes
Log in to take notes.