Superglobals

OxPHP populates every standard PHP superglobal before your script executes, the same way a traditional server setup does. Values are available from the first line of your code, and you do not need to initialize anything. Only one setting narrows that; see Disabling superglobals.

$_SERVER

OxPHP builds $_SERVER from the incoming HTTP request following the CGI/1.1 specification. Process environment variables are imported first; CGI variables are set afterward, so request-specific values always override any colliding environment keys.

Standard variables

Variable Description Example
SCRIPT_FILENAME Absolute filesystem path to the PHP script being executed /var/www/html/public/index.php
DOCUMENT_ROOT Web root directory configured via DOCUMENT_ROOT env var /var/www/html/public
SERVER_SOFTWARE Server identifier (carries the running OxPHP version) OxPHP/0.13.0
SERVER_PROTOCOL Negotiated HTTP protocol version HTTP/2
REQUEST_METHOD HTTP method GET
REQUEST_URI Full URI with query string /app?page=2
SCRIPT_NAME Path of the executed script relative to DOCUMENT_ROOT. In Framework mode this is the front controller, not the request URI /index.php
DOCUMENT_URI Alias for SCRIPT_NAME, for nginx/PHP-FPM compatibility /index.php
PHP_SELF SCRIPT_NAME plus PATH_INFO when present, otherwise equal to SCRIPT_NAME /index.php/user/42
QUERY_STRING Query portion of the URI (empty string when absent) page=2
SERVER_NAME Hostname from the Host header example.com
SERVER_PORT Port from the Host header 8080
REMOTE_ADDR Client IP address 172.17.0.1
REMOTE_PORT Client port number 54321
HTTPS Set to "on" when the connection uses TLS; absent otherwise on
REQUEST_SCHEME "https" for TLS connections, "http" otherwise https
CONTENT_TYPE Value of the Content-Type header (no HTTP_ prefix) application/json
CONTENT_LENGTH Value of the Content-Length header (no HTTP_ prefix) 128
REQUEST_TIME Unix timestamp (integer) when the request started 1738800000
REQUEST_TIME_FLOAT Unix timestamp with microsecond precision 1738800000.123456
GATEWAY_INTERFACE CGI version string CGI/1.1

When the Host header is absent, SERVER_NAME defaults to localhost and SERVER_PORT defaults to 80 (or 443 for TLS).

HTTP request headers

All HTTP request headers are added to $_SERVER with an HTTP_ prefix. Header names are converted to uppercase with dashes replaced by underscores, following CGI/1.1 conventions:

text
Accept: text/html -> HTTP_ACCEPT X-Forwarded-For: 1.2.3.4 -> HTTP_X_FORWARDED_FOR Authorization: Bearer abc -> HTTP_AUTHORIZATION Cookie: session=xyz -> HTTP_COOKIE
Note

Content-Type and Content-Length appear without the HTTP_ prefix, as CONTENT_TYPE and CONTENT_LENGTH, because the CGI specification requires it.

Behind a reverse proxy

When TRUSTED_PROXIES is configured and the request peer is in the trusted set, OxPHP rewrites the following $_SERVER keys from forwarded headers (X-Forwarded-* or RFC 7239 Forwarded):

Variable Value when peer is trusted Value otherwise
REMOTE_ADDR Rightmost untrusted address from X-Forwarded-For / Forwarded; CF-Connecting-IP for a request whose forwarding chain shows it arrived from a Cloudflare edge when TRUSTED_PROXIES names cloudflare; the direct peer when the forwarding headers yield no address Direct peer IP
REMOTE_PORT Client source port from an ip:port entry in Forwarded or X-Forwarded-For, otherwise 0; the peer's port when the address is not rewritten Direct peer port
HTTPS "on" when X-Forwarded-Proto: https Set only when the peer connection is TLS
REQUEST_SCHEME "https" / "http" from X-Forwarded-Proto Based on actual TLS state
SERVER_NAME Host portion of X-Forwarded-Host Host portion of the Host header
SERVER_PORT X-Forwarded-Port, else port portion of X-Forwarded-Host, else 443/80 by scheme Port portion of Host, or 443/80

The raw HTTP_X_FORWARDED_FOR, HTTP_X_FORWARDED_PROTO, HTTP_X_FORWARDED_HOST, HTTP_X_FORWARDED_PORT, and HTTP_FORWARDED keys remain in $_SERVER unchanged, so both the rewritten values and the original headers are available.

When the client address is rewritten, REMOTE_PORT is the client's source port if the entry the address comes from carries one (an RFC 7239 Forwarded: for="ip:port" node, or an ip:port entry in X-Forwarded-For), and "0" otherwise: the value is zeroed rather than guessed. An address taken from CF-Connecting-IP carries no port either, so REMOTE_PORT is "0" then even when Forwarded has one. When the forwarding headers yield no client address (see Client IP extraction), neither REMOTE_ADDR nor REMOTE_PORT is rewritten: both stay those of the direct peer.

When TRUSTED_PROXIES is not set, no rewriting occurs and REMOTE_ADDR is always the direct peer, which is typically your load balancer rather than the end client. Parsing X-Forwarded-For manually is error-prone (leftmost vs rightmost, no CIDR trust check); prefer configuring TRUSTED_PROXIES. See Trusted Proxies for the trust algorithm and configuration syntax.

Trace context variables

When distributed tracing is enabled, OxPHP adds trace context variables to $_SERVER:

Variable Description Example
OXPHP_TRACE_ID W3C trace ID for the current request 4bf92f3577b34da6a3ce929d0e0e4736
OXPHP_SPAN_ID Span ID for the OxPHP server span 00f067aa0ba902b7
OXPHP_PARENT_SPAN_ID Parent span ID from the upstream service (empty if root) b9c7c989f97918e1

These variables are only present when a valid traceparent header arrives or when OxPHP generates a new trace. If tracing is not configured, these keys are absent.

Differences from PHP-FPM

The following variables behave differently compared to a standard PHP-FPM setup:

Variable Behavior
SERVER_ADDR Not set. OxPHP does not populate the local server IP address.
PATH_INFO Set automatically. See PATH_INFO behavior below.
PATH_TRANSLATED Not set.
PHP_AUTH_USER / PHP_AUTH_PW / AUTH_TYPE Not extracted from the Authorization header. Read $_SERVER['HTTP_AUTHORIZATION'] directly.
REDIRECT_STATUS Not set. OxPHP does not use an internal redirect mechanism.

Example

php
<?php $method = $_SERVER['REQUEST_METHOD']; $uri = $_SERVER['REQUEST_URI']; $ip = $_SERVER['REMOTE_ADDR']; $host = $_SERVER['SERVER_NAME']; $scheme = $_SERVER['REQUEST_SCHEME']; // "http" or "https" // Read a custom header $token = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; // REMOTE_ADDR is already the real client IP when TRUSTED_PROXIES is configured. // Without it, REMOTE_ADDR is the direct peer (usually a load balancer). $clientIp = $_SERVER['REMOTE_ADDR']; // Check TLS without checking the port if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { // Secure connection }

PATH_INFO behavior

$_SERVER['PATH_INFO'] is populated automatically based on the active routing mode. There is no feature flag; the previous SPLIT_PATH_INFO_ENABLED env variable has been removed.

Routing mode When set Value
Traditional (ENTRY_FILE unset) Only when the URI contains .php/ and the script prefix exists on disk Tail after the script segment
Framework (ENTRY_FILE=index.php) Only when the request explicitly names the entry file with a trailing segment (/index.php/extra) Tail after the entry file, e.g. /news
SPA (ENTRY_FILE=index.html) Never. PHP only runs for exact .php files, so there is no PATH_INFO —

SCRIPT_NAME always identifies the executed script (the resolved file relative to the document root), so in normal routing PATH_INFO is present only when SCRIPT_NAME is a literal prefix of the request path. When a request is rewritten to a front controller it does not name (an application route, a directory index, a static-miss fallback), PATH_INFO is absent and the original path lives in REQUEST_URI. (The PHP_DENY_PATHS fallback is a deliberate exception: it sets PATH_INFO to the original sanitized URI so the fallback script can route on it.)

Traditional mode examples

OxPHP scans the URI left-to-right for the first .php segment that corresponds to an actual file on disk. Everything after it becomes PATH_INFO:

Request URI File on disk SCRIPT_NAME PATH_INFO PHP_SELF
/app.php/user/42 app.php exists /app.php /user/42 /app.php/user/42
/index.php/api/v2/users index.php exists /index.php /api/v2/users /index.php/api/v2/users
/app.php app.php exists /app.php (absent) /app.php
/missing.php/foo file not found falls back to /index.php — depends on fallback

Framework mode examples

Every non-static request is rewritten onto index.php. PATH_INFO is set only when the request explicitly names the entry file with a trailing segment; for application routes the original path is read from REQUEST_URI.

Request URI SCRIPT_NAME PATH_INFO
/api/users /index.php (absent)
/about.php /index.php (absent)
/index.php/news/local /index.php /news/local
/index.php /index.php (absent)
Note

PATH_TRANSLATED is not populated. It is rarely used in practice and is not set by nginx or PHP-FPM by default.

$_GET

Query string parameters are parsed automatically from the request URI.

php
<?php // Request: GET /search?q=oxphp&page=2 $query = $_GET['q']; // "oxphp" $page = $_GET['page']; // "2"

Array syntax works as expected:

php
<?php // Request: GET /filter?tags[]=php&tags[]=async $tags = $_GET['tags']; // ["php", "async"]
What filter_input(INPUT_GET, …) sees

filter_input(), filter_input_array() and filter_has_var() do not read $_GET, $_POST or $_COOKIE. The filter extension keeps a copy of the parsed input of its own, and in a persistent worker that copy is filled by every request, so it has to be given back by every request too. Otherwise one client's query values, session cookies and body fields stay readable to every request the worker serves afterwards. OxPHP gives it back at the start of each request, so these three functions answer for the request asking and for no other.

Worker mode adds one limit. If your request pauses (sleep(), an await, a hooked call) and another request runs on that worker in the window, the storage becomes that request's. Reads after the resume answer null rather than someone else's input, and the copy your request had is gone by then. The other request does not have to finish; starting or resuming on that worker is enough. $_GET, $_POST and $_COOKIE travel with the request across a suspension and are unaffected, so read those, or read the filter functions before you suspend.

One call is not allowed to pause at all. filter_input_array() given a per-field definition array reads the storage once per field, so a FILTER_CALLBACK that does I/O runs with the worker held for its duration instead of handing it to another request, and an explicit Fiber::suspend() inside such a callback throws. The wait still happens, but the worker thread waits rather than the request, so a slow callback shows up as latency for everything queued behind it, and past their wait budget those queued requests are shed. How long it can last is set by whatever the callback is talking to, not by the server: a stream wrapper gives up after default_socket_timeout (60 seconds out of the box), while mysqlnd waits mysqlnd.net_read_timeout (a day out of the box). Give such a call an explicit timeout. Better still, run validation that reaches a database or a cache on the value after filter_input_array() has returned it.

$_POST

OxPHP supports the two standard content types for form submissions:

  • application/x-www-form-urlencoded: standard HTML form data
  • multipart/form-data: file uploads combined with form fields
php
<?php // Request: POST /login // Content-Type: application/x-www-form-urlencoded // Body: username=admin&password=secret $username = $_POST['username']; // "admin" $password = $_POST['password']; // "secret"

For JSON or other content types, use php://input instead:

php
<?php // Request: POST /api/users // Content-Type: application/json // Body: {"name":"Alice","email":"alice@example.com"} $data = json_decode(file_get_contents('php://input'), true); $name = $data['name']; // "Alice" $email = $data['email']; // "alice@example.com"
Note

filter_input(INPUT_POST, …) reads the filter extension's own copy of the body rather than $_POST. See the note under $_GET for what that copy is and how long it lasts.

Cookies are parsed from the Cookie request header.

php
<?php // Request with: Cookie: session=abc123; theme=dark $session = $_COOKIE['session']; // "abc123" $theme = $_COOKIE['theme']; // "dark"
Note

Cookies with the __oxp_ prefix are reserved for internal OxPHP plugins. They are stripped from the Cookie header before it reaches PHP and will not appear in $_COOKIE.

Note

filter_input(INPUT_COOKIE, …) reads the filter extension's own copy of the cookies rather than $_COOKIE (the note under $_GET explains what that copy is and how long it lasts).

$_FILES

File uploads sent via multipart/form-data populate the $_FILES array with the standard PHP structure:

php
<?php // $_FILES['avatar'] structure: // [ // 'name' => 'photo.jpg', // Original filename sent by the client // 'type' => 'image/jpeg', // MIME type declared by the client // 'tmp_name' => '/tmp/phpAb12Cd', // Temporary file path on the server // 'error' => 0, // UPLOAD_ERR_OK (0 means no error) // 'size' => 204800, // File size in bytes // ] if ($_FILES['avatar']['error'] === UPLOAD_ERR_OK) { $tmp = $_FILES['avatar']['tmp_name']; $name = basename($_FILES['avatar']['name']); move_uploaded_file($tmp, "/uploads/$name"); }

$_REQUEST

$_REQUEST is a merged array of $_GET, $_POST and $_COOKIE, built by PHP according to the request_order INI directive, or variables_order when request_order is not set. The merge itself follows PHP's rules unchanged: the sources are merged in the order the directive lists them, and a later source overwrites a key an earlier one set.

Cookies are included, and they win

The OxPHP images load no php.ini: php.ini-production and php.ini-development sit in /usr/local/etc/php unused (see Installation). As a result, request_order is unset and PHP falls back to its built-in variables_order, "EGPCS": GET, then POST, then COOKIE. A cookie therefore replaces a query or body value of the same name; when both sides are arrays (k[a] in the body, k[b] in a cookie) they are merged key by key under the same rule.

The request_order = "GP" you may know from PHP-FPM setups comes from the php.ini-production and php.ini-development samples, not from PHP itself. To leave cookies out, set it in an ini file loaded at startup, for example /usr/local/etc/php/conf.d/zz-request-order.ini containing request_order = "GP". ini_set() cannot change this directive at runtime.

php
<?php // POST /form?action=preview with body action=submit $action = $_REQUEST['action']; // "submit" — POST overrides GET // Same request, also carrying the header "Cookie: action=delete" $action = $_REQUEST['action']; // "delete" — the cookie overrides both
Worker mode

$_REQUEST is rebuilt for every request. PHP normally builds it lazily, once, when a script that mentions it is first loaded. In a persistent worker that would mean every later request reading the first one's parameters. OxPHP forces the rebuild instead, so the merged array always describes the request being served.

$_ENV

$_ENV holds the process environment. In traditional mode it behaves exactly as in PHP-FPM: PHP repopulates it from the environment on every request, subject to variables_order.

Worker mode pins it. A worker bootstraps once, and .env loaders (vlucas/phpdotenv, symfony/dotenv, Laravel's Env) write their values straight into $_ENV without touching the process environment. Repopulating $_ENV per request would therefore erase the application's configuration from its second request onward, so in worker mode the array is kept for the life of the worker once it exists. The writes your bootstrap makes there stay visible to every request that worker serves, and so does any other write into it; the warning below the example explains what that means inside a request handler.

php
<?php // bootstrap, before oxphp_worker() Dotenv\Dotenv::createImmutable(__DIR__)->load(); // writes into $_ENV oxphp_worker(function () { echo $_ENV['DATABASE_URL']; // still there on request 10_000 });
In worker mode $_ENV is worker state, not request state

The mechanism only pins the array, so it does not distinguish a bootstrap write from a handler write: $_ENV['tenant_id'] = $request->tenant(); inside a request handler stays in $_ENV for the rest of that worker's life. Every later request that worker serves reads it. So does a request the worker takes on while the writer is parked at one of the suspension points, because $_ENV is deliberately left out of the state a suspended request carries with it, and the two then read the same array. Neither case is reported. No warning or log entry is produced; a request simply reads a value that another request set.

Write to $_ENV from your bootstrap and nowhere else. Store a per-request value somewhere that is rebuilt per request. $_SERVER is, and it travels with its own request across a suspension. Anything your framework scopes to the request also works, but check that it is scoped to the request rather than to the worker; a container binding or a request stack set up once during bootstrap is worker state, with the same reach as $_ENV. The attributes container on OxPHP's own request object also works, as long as you pass that object along: oxphp_http_request() builds a fresh Request on every call, and a fresh, empty container with it.

What filter_input(INPUT_ENV, …) sees

filter_input(INPUT_ENV, …), filter_input_array(INPUT_ENV) and filter_has_var(INPUT_ENV, …) read the process environment in worker mode. They do not read $_ENV, so values written there, by your bootstrap or by anything else, are not among them. That is how PHP behaves everywhere, because writing to $_ENV gives the array its own copy and the filter extension goes on reading the environment snapshot the engine took. Worker mode adds one wrinkle: the snapshot is the one taken when $_ENV was first built on the worker, so a putenv() call made afterwards is not in it either. getenv() reads the live environment and is unaffected; $_ENV holds the process values plus whatever PHP on that worker has written into it. All of this is about INPUT_ENV alone: INPUT_GET, INPUT_POST and INPUT_COOKIE read storage of their own inside the filter extension, described in the note under $_GET above.

The pinning relies on PHP's default auto_globals_jit=1. With auto_globals_jit=0, PHP repopulates $_ENV from the process environment on every request before any extension can intervene, and a .env loader's values will not survive in worker mode.

php://input

The raw request body is available through the php://input stream. This is the standard way to read JSON payloads, XML, or any content type other than form submissions.

php
<?php $body = file_get_contents('php://input'); $data = json_decode($body, true);

php://input is rewindable and can be read multiple times within the same request.

Note

php://input is empty for multipart/form-data requests. Use $_POST and $_FILES for those.

Disabling superglobals

Set SUPERGLOBALS_ENABLED=false to stop OxPHP describing the request to PHP: the CGI and HTTP_* variables are not registered, and the query string is not handed over to be parsed. Use the HTTP Request API (oxphp_http_request()) to access request data instead.

bash
SUPERGLOBALS_ENABLED=false # the $_SERVER request keys and $_GET are not built

It reaches fewer of these arrays than the name suggests. The request body and the Cookie header are handed to PHP either way, so everything PHP builds out of them is there as usual:

What With SUPERGLOBALS_ENABLED=false
$_GET Empty
$_SERVER During a request, only four keys: REQUEST_TIME, REQUEST_TIME_FLOAT, argc and argv, all registered by PHP itself rather than by the server. REQUEST_METHOD, REQUEST_URI, the HTTP_* keys and the process environment are absent. Worker mode bootstraps differently: the code above oxphp_worker() runs before any request and sees a $_SERVER built as though the setting were on, the whole process environment included, plus a placeholder REQUEST_URI of /
$_POST, $_FILES, $_COOKIE Populated as usual
$_REQUEST Populated, merged by PHP as always (see $_REQUEST); only what $_GET would have contributed is missing
$_SESSION Filled by PHP's session module when a request starts a session, not by the SAPI
php://input A stream, not a superglobal; readable as usual
header(), headers_list(), etc. SAPI functions, not superglobals
session_start() and other session_*() functions Native PHP functions
oxphp_http_request() Available; every method answers, including query() and payload()
Routing on REQUEST_URI

Code that routes on $_SERVER['REQUEST_URI'] finds nothing there under this setting and has to read oxphp_http_request()->path() instead. In a worker entry script this applies to the handler. The bootstrap above it sees the placeholder / described in the table, which is not the real route either.

You can check the current setting at runtime:

php
if (!oxphp_superglobals_enabled()) { $request = oxphp_http_request(); $page = $request->query('page', 1); }

Prompts for your agent

These prompts assume an agent with the OxPHP documentation MCP server connected. The agent reads the pages it needs itself.

prompt
Audit my code's superglobal reads for what differs between OxPHP and PHP-FPM. Read the `php/superglobals` page through the OxPHP documentation MCP server. Check my application for `$_SERVER['PHP_AUTH_USER']`, `SERVER_ADDR`, or `PATH_TRANSLATED` reads and for Basic-Auth code expecting them pre-parsed, then tell me which reads need to change and to what.
prompt
My application under OxPHP worker mode is losing values it read through `filter_input()`. Read the `php/superglobals` page through the OxPHP documentation MCP server. Check my code for `filter_input()` and `filter_input_array()` calls that pause on I/O or a suspended Fiber, and tell me which ones are exposed to the worker-mode race the page describes, where a resumed request reads another request's storage.

See also

  • HTTP Request API: typed, lazy-loading request object as an alternative to superglobals
  • PHP Functions: oxphp_request_id(), oxphp_worker_id(), and other extension functions
  • Worker Mode: how superglobals are refreshed between worker requests
  • Configuration Reference: DOCUMENT_ROOT and other server configuration variables
Found a mistake? Report it →