Skip to content
academia.sh

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 $_GET gives null and reports an E_WARNING; execution continues.
  • The null coalescing operator moves the same absence to the silent channel; filter_input on the command line returns null and so announces it through the return channel.
  • Request data arrives as string, a name in the form a[] makes it an array; in a POST request the query string lands in $_GET and 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 strlen throw a TypeError, 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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close