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:
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_COOKIEContent-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
$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) |
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
// Request: GET /search?q=oxphp&page=2
$query = $_GET['q']; // "oxphp"
$page = $_GET['page']; // "2"Array syntax works as expected:
<?php
// Request: GET /filter?tags[]=php&tags[]=async
$tags = $_GET['tags']; // ["php", "async"]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 datamultipart/form-data: file uploads combined with form fields
<?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
// 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"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.
$_COOKIE
Cookies are parsed from the Cookie request header.
<?php
// Request with: Cookie: session=abc123; theme=dark
$session = $_COOKIE['session']; // "abc123"
$theme = $_COOKIE['theme']; // "dark"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.
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
// $_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.
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
// 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$_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
// 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
});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.
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
$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.
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.
SUPERGLOBALS_ENABLED=false # the $_SERVER request keys and $_GET are not builtIt 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() |
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:
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.
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.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_ROOTand other server configuration variables