---
title: Superglobals
source: 'https://academia.sh/en/courses/php-fundamentals/superglobals'
course: 'PHP Fundamentals'
language: en
updated: '2026-10-21T05:03:34+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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'
---

# 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.

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
<?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
<?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
<?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";
```

```bash
# 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
<?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";
```

```bash
# 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
<?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);
}
```

```bash
# 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
<?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";
```

```bash
# 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
<?php
// early.php — the error arrives before any output starts
$u = strlen($_GET['u']);
echo "length=$u\n";
```

```php
<?php
// late.php — the same error arrives after an echo
echo "started\n";
$u = strlen($_GET['u']);
echo "length=$u\n";
```

```bash
# 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.
