HTTP Request API
OxPHP provides an object-oriented API for accessing HTTP request data. Instead of reading $_GET, $_POST, $_COOKIE, $_FILES, and $_SERVER, you call methods on a Request object that returns exactly what you ask for.
Table of contents
- Overview
- Getting the Request object
- RequestInterface methods
- SessionInterface
- UploadedFileInterface
- AttributesInterface
- Exceptions
- SUPERGLOBALS_ENABLED
- Worker Mode
- IDE support
- Examples
Overview
oxphp_http_request() returns a read-only proxy to the HTTP request data stored in the current worker thread. Data is fetched lazily. A single method call like $request->header('Accept') goes directly to the Rust-side data structure and returns just that value. Of the full-array calls, query() and payload() keep their parsed result on the object they were called on; headers(), cookies() and files() build theirs afresh every time. Nothing is kept for the request: each call to oxphp_http_request() returns a new object, with a new set of those caches.
Why use it instead of superglobals?
- JSON body parsing is built in.
$request->payload()hands back a decodedapplication/jsonbody whatever the request method is, and the fields of aPOSTform body (application/x-www-form-urlencodedormultipart/form-data) without extra code. The exact returns are in Parsed body. - There are no array keys to mistype.
$request->method()is harder to get wrong than$_SERVER['REQUEST_METHOD']. - File uploads are type-detected.
$request->file('avatar')->type()returns the MIME type determined from the file's actual contents, not the client-supplied value. - Behavior is defined by interfaces, so you can inject mock implementations in unit tests.
- Superglobals stay available. Setting
SUPERGLOBALS_ENABLED=falseis optional, and the object API works regardless.
Getting the Request object
<?php
$request = oxphp_http_request();Call oxphp_http_request() anywhere in a script executing inside an active HTTP request, including inside the oxphp_worker() callback:
<?php
oxphp_worker(function () {
$request = oxphp_http_request();
$method = $request->method();
// ...
});RequestInterface methods
URI and method
$request->method(): stringReturns the HTTP method in uppercase: "GET", "POST", "PUT", "PATCH", "DELETE", etc.
$request->isMethod(string $method): boolCase-insensitive method check.
$request->path(): stringThe URI path without the query string: "/users/42".
$request->fullUri(): stringThe complete URI including scheme, host, optional non-standard port, path, and query string: "https://example.com:8080/users/42?page=2". Standard ports (80 for HTTP, 443 for HTTPS) are omitted.
$request->scheme(): string"https" or "http".
$request->isSecure(): booltrue when the scheme is "https".
$request->host(): stringHostname from the Host header. Returns an empty string when the header is absent (HTTP/1.0 requests without a Host header).
$request->port(): intPort from the Host header. When not explicitly present, returns the default for the scheme: 80 for HTTP, 443 for HTTPS.
scheme(), isSecure(), host(), and port() honor X-Forwarded-Proto and X-Forwarded-Host when TRUSTED_PROXIES includes the peer. Without trusted proxies they reflect the direct connection.
$request->queryString(): ?stringThe raw query string without the leading ?. Returns null when there is no query string.
Protocol
$request->httpProtocol(): stringThe full protocol string: "HTTP/1.1" or "HTTP/2".
$request->httpProtocolVersion(): stringThe version number only: "1.1" or "2".
Query parameters
$request->query(?string $key = null, mixed $default = null): mixedAccess query string parameters.
| Call | Returns |
|---|---|
$request->query() |
All parameters as a flat array of name-value pairs |
$request->query('page') |
The value of page, or null if absent |
$request->query('page', 1) |
The value of page, or 1 if absent |
Bracket notation is not expanded into nested arrays yet. ?tags[]=php&tags[]=async currently yields a flat entry named tags[] rather than a tags array, and a repeated name resolves to its first occurrence through query('name') while query() keeps the last. Use $_GET for array parameters until this is fixed.
// Request: GET /search?q=oxphp&tags[]=php&tags[]=async
$q = $request->query('q'); // "oxphp"
$tags = $_GET['tags']; // ["php", "async"]Found values are always strings. $default is returned as-is when the key is absent. A parameter sent without a value (?flag) and one sent empty (?flag=) both read as ''. Only an absent parameter gives null.
Decoding
Names and values are percent-decoded, and + is read as a space. PHP applies the same rules when building $_GET:
// Request: GET /search?q=%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82&tag=a+b
$request->query('q'); // "Привет"
$request->query('tag'); // "a b"Values are byte strings, so an escape encoding something that is not valid UTF-8 survives intact. A signed token or a binary id can still be verified against the bytes as sent. queryString() remains the way to read the query string exactly as it arrived, undecoded.
Names are reported as the client sent them, where $_GET reports them as PHP files them. PHP rewrites ' ', '.' and '[' in superglobal names, so ?a.b=1 lands in $_GET as a_b:
// Request: GET /?a.b=1
$_GET['a_b']; // "1"
$request->query('a.b'); // "1"A name is also kept whole where PHP would cut it. PHP handles superglobal names as C strings, so ?a%00b=1 becomes $_GET['a'] while query() keeps a\0b. Do not read that as a reason to validate through one API and read through the other: the truncation lets one parameter land on another's key, so ?a=1&a%00x=2 gives $_GET['a'] === '2' where query('a') === '1', and the two disagree on the value rather than merely on the set of names.
At most 1000 parameters are parsed. PHP applies its own limit, max_input_vars, which defaults to the same 1000 but is configurable. If you raise it, $_GET will hold parameters past the point where query() stops.
Parsed body
$request->payload(?string $key = null, mixed $default = null): mixedReturns the parsed request body. The body is parsed according to the Content-Type header:
| Content-Type | Returns |
|---|---|
application/x-www-form-urlencoded |
Associative array of field values, on a POST request; an empty array on any other method |
multipart/form-data |
Associative array of text field values, on a POST request; an empty array on any other method |
application/json |
The decoded value: an array for an object or an array, otherwise the scalar itself (string, int, float, bool); null for invalid JSON and for a literal null body |
| Any other value | null. Matching is by prefix, so anything beginning application/json (application/json-patch+json, for example) takes the JSON row above, while text/json and application/vnd.api+json land here |
A JSON body is decoded whatever the request method is. A form body is not: the two form rows read what PHP parsed the body into, and PHP does that for POST alone, so payload() on a PUT or PATCH carrying form fields returns an empty array. Send such bodies as JSON, or read them from body() and parse them yourself.
For both form types, payload() returns the fields as PHP parsed them from the body. A later write to $_POST changes only the script's copy of the array, and payload() does not see it.
The parsed result is cached on the first call and reused by later calls on the same object. Each oxphp_http_request() call returns a separate object with its own cache, so a second object builds the result again.
| Call | Returns |
|---|---|
$request->payload() |
The entire parsed body |
$request->payload(null, []) |
The entire parsed body, or [] when there is none: an empty body, a Content-Type from the last row above, invalid JSON, or a literal JSON null |
$request->payload('email') |
A single field value, or null if absent |
$request->payload('email', '') |
A single field value, or '' if absent |
A key lookup only reaches into an array payload. On a JSON body that decoded to a scalar there are no keys, so payload('anything') returns the default. A body that decodes to false or 0 is returned as that value; the default does not replace it.
<?php
// JSON request: POST /api/users
// Content-Type: application/json
// Body: {"name": "Alice", "role": "admin"}
$name = $request->payload('name'); // "Alice"
$role = $request->payload('role'); // "admin"
$data = $request->payload(); // ["name" => "Alice", "role" => "admin"]Headers
$request->header(string $name, ?string $default = null): ?stringReturns the raw header value. Header names are case-insensitive. For multi-value headers (Accept, X-Forwarded-For), the full header line is returned as a single string. Parsing is your responsibility.
$request->hasHeader(string $name): boolReturns true if the named header is present.
$request->headers(): arrayReturns all headers as an associative array. Each key is the header name as received (no normalization), and each value is the raw header string.
<?php
$accept = $request->header('Accept');
// "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
if ($request->hasHeader('Authorization')) {
$token = $request->header('Authorization');
}
$all = $request->headers();
// ["Content-Type" => "application/json", "Accept" => "...", ...]Cookies
$request->cookie(string $name, ?string $default = null): ?stringReturns the value of a single cookie, or $default if the cookie is not present. A cookie sent empty (Cookie: a=) reads as ''; only an absent cookie gives $default.
$request->cookies(): arrayReturns all cookies as an associative array of name-value pairs.
Decoding
Cookies decode differently from query parameters, following what PHP does when building $_COOKIE: values are percent-decoded, but + stays a literal +, and names are not decoded at all.
// Cookie: token=a%20b; sep=a+b; %D0%BA=1
$request->cookie('token'); // "a b" — percent-escapes decoded
$request->cookie('sep'); // "a+b" — unlike query(), "+" is not a space
$request->cookie('%D0%BA'); // "1" — look up by the name as sentNames are reported as sent here too. PHP rewrites ' ', '.' and '[' in every superglobal name, cookies included, so Cookie: a.b=1 lands in $_COOKIE as a_b while cookie('a.b') and cookies()['a.b'] use the name the client sent.
Values are byte strings, so a signed cookie or setcookie('sid', random_bytes(16)) round-trips as the exact bytes it was set with.
<?php
$theme = $request->cookie('theme', 'light'); // "dark" or "light"
$session = $request->cookie('session'); // null if absent
$all = $request->cookies(); // ["theme" => "dark", ...]Raw body
$request->body(): stringReturns the raw request body bytes. This is the OxPHP equivalent of file_get_contents('php://input'). Unlike payload(), body() is not cached. Each call goes to the underlying data structure.
$request->contentType(): ?stringReturns the value of the Content-Type header, or null if absent.
<?php
// Read raw body for signature verification
$raw = $request->body();
$signature = $request->header('X-Hub-Signature-256');
$valid = hash_hmac('sha256', $raw, $secret) === $signature;body() and payload() are independent. You can call both in the same request.
File uploads
$request->file(string $name): ?UploadedFileInterfaceReturns the uploaded file for the given field name, or null if the field is not present. For array fields (name="photos[]"), returns the first file.
$request->files(?string $name = null): array| Call | Returns |
|---|---|
$request->files() |
All uploaded files as a flat array of UploadedFileInterface |
$request->files('photos') |
All files for the photos field (supports name="photos[]") |
Both build new UploadedFileInterface objects on every call. Two file('avatar') calls return two objects for the same uploaded file, and a type one of them has detected with type() is not known to the other. Keep the object you got if you need it more than once, as the example below does.
<?php
$avatar = $request->file('avatar');
if ($avatar && $avatar->isValid()) {
$mime = $avatar->type(); // Detected from file contents, not the client claim
$name = $avatar->name(); // Original filename
$avatar->moveTo('/var/uploads/' . basename($name));
}
// Multiple files
$photos = $request->files('photos'); // UploadedFileInterface[]
foreach ($photos as $photo) {
if ($photo->isValid()) {
$photo->moveTo('/var/uploads/' . basename($photo->name()));
}
}Client
$request->ip(): stringReturns the client IP address. When TRUSTED_PROXIES is configured and the request peer is in the trusted set, this is the rightmost untrusted address from X-Forwarded-For or RFC 7239 Forwarded. When TRUSTED_PROXIES names cloudflare and the forwarding chain shows the request arrived from a Cloudflare edge, it is the CF-Connecting-IP address instead. Otherwise it is the direct peer IP, typically your load balancer, not the end client.
The raw X-Forwarded-For header remains available via $request->header('X-Forwarded-For') for advanced cases, but parsing it manually is rarely correct (leftmost vs rightmost, no CIDR trust check). Configure TRUSTED_PROXIES instead. See Trusted Proxies.
Timing
$request->startTime(bool $asFloat = false): int|floatReturns the Unix timestamp for when this request was received.
| Call | Returns |
|---|---|
$request->startTime() |
Integer seconds: 1711234567 |
$request->startTime(true) |
Float with sub-second precision: 1711234567.3412 |
<?php
$elapsed = microtime(true) - $request->startTime(true);
error_log(sprintf("Request took %.3fs so far", $elapsed));Attributes
$request->attributes(): AttributesInterfaceReturns the mutable attributes container held by this Request object. Use attributes to hand data from middleware to route handlers and to any other code that receives the same object, instead of using a global variable.
<?php
// In authentication middleware
$request->attributes()->set('user', $authenticatedUser);
// In the route handler
$user = $request->attributes()->get('user');oxphp_http_request() returns a new Request on every call, and the container you take from it is a new, empty one. A write made through one call is not readable through another: get() returns the default instead, and nothing is warned or logged.
<?php
oxphp_http_request()->attributes()->set('tenant_id', $id);
oxphp_http_request()->attributes()->get('tenant_id'); // null — a second Request, a second containerIf you pass the object along, as the example above does with $request, all code that holds it reads and writes the same container, including a Fiber you pass it to. A Fiber that calls oxphp_http_request() for itself gets a container of its own, like any other caller. Because PHP Fibers are cooperative, no two of them access a container at the same time.
No reset between requests is needed. The container is freed together with the object that holds it, and the next request's handler calls oxphp_http_request() and gets its own.
Session
$request->session(): ?SessionInterfaceReturns a read-only view of $_SESSION. Returns null if session_start() has not been called. Session management (starting, saving, destroying, writing values) uses the standard PHP session functions.
<?php
session_start();
$session = $request->session();
$userId = $session->get('user_id');
$isAdmin = $session->get('is_admin', false);
// Write session data using standard PHP functions
$_SESSION['last_seen'] = time();SessionInterface
SessionInterface is a read-only view of the active session.
namespace OxPHP\Http;
interface SessionInterface
{
public function id(): string;
public function name(): string;
public function get(string $key, mixed $default = null): mixed;
public function has(string $key): bool;
public function all(): array;
}| Method | Description |
|---|---|
id() |
The session ID |
name() |
The session name (default: "PHPSESSID") |
get(key, default) |
A single session value, or $default if the key is absent |
has(key) |
true if the key exists in $_SESSION |
all() |
All session data as an array |
Session values reflect the current state of $_SESSION at the time of the call, not the state when session() was first called.
UploadedFileInterface
UploadedFileInterface represents a single uploaded file.
namespace OxPHP\Http;
interface UploadedFileInterface
{
public function name(): string;
public function clientType(): string;
public function type(): string;
public function size(): int;
public function tmpPath(): string;
public function error(): int;
public function isValid(): bool;
public function moveTo(string $destination): bool;
}| Method | Description |
|---|---|
name() |
Original filename sent by the client |
clientType() |
MIME type declared by the client. Do not trust this value for security decisions |
type() |
MIME type determined from the file's actual contents using magic byte detection (mime_content_type(), from ext/fileinfo). Returns "application/octet-stream" when the type cannot be determined. Detected on the first call and kept on this object (see Type detection). |
size() |
File size in bytes |
tmpPath() |
Path to the temporary file on disk |
error() |
One of the UPLOAD_ERR_* constants |
isValid() |
true when error() is UPLOAD_ERR_OK |
moveTo(path) |
Moves the file to $path with move_uploaded_file(), without detecting its type. Returns false if the file is invalid or the move fails. What type() reads after a move is under Type detection. |
Always check isValid() before using an uploaded file. Use type() rather than clientType() when making security-sensitive decisions:
<?php
$file = $request->file('document');
if (!$file || !$file->isValid()) {
http_response_code(400);
echo json_encode(['error' => 'Upload failed or missing']);
return;
}
$detectedMime = $file->type();
$allowed = ['application/pdf', 'image/jpeg', 'image/png'];
if (!in_array($detectedMime, $allowed, true)) {
http_response_code(415);
echo json_encode(['error' => "File type not allowed: $detectedMime"]);
return;
}
$file->moveTo('/var/uploads/' . bin2hex(random_bytes(8)) . '.pdf');Type detection
The first type() call reads the file and keeps the result on the object; later calls on that object return it without reading the file again. That includes "application/octet-stream" after a failed detection, also when the first call threw under an error handler that turns warnings into exceptions: later calls return it without a warning. An object from another file() or files() call reads the file again on its own first call.
Without mime_content_type() (ext/fileinfo not loaded, or the function listed in disable_functions), every call that would read the file raises a warning and returns "application/octet-stream" without keeping it, or throws under an error handler that turns warnings into exceptions. OxPHP also logs one line about it per process at warn level, in the server log or where error_log points when it is set. The warnings themselves are logged like any other PHP warning.
moveTo() does not detect the type. If type() was not called on the object before the move, its first call after a successful move reads the file at the destination:
- A local path is the moved file, found by the absolute path the move put it at. A later change of working directory does not matter, including the one PHP makes itself before shutdown functions and destructors run.
- A stream-wrapper URL is opened again through its wrapper, and the type is detected from what that returns. Where the wrapper cannot open the URL for reading (with a warning) or cannot
statit (compress.zlib://, for one; with a warning on PHP 8.4),type()returns"application/octet-stream", or throws under an error handler that turns warnings into exceptions. - A
data:URL is never decoded.move_uploaded_file()renames to it as a local file name, whichtype()reads, or, where that fails, copies to the URL, which takes no writes: only an empty upload gets through, with nothing written, andtype()finds no file. - Another object for the same upload whose
type()was not called before the move (one fetched again withfile(), for instance) finds no file.mime_content_type()raises a warning for the missing file, and that object'stype()returns"application/octet-stream", or throws under an error handler that turns warnings into exceptions.
Call type() before moveTo() when the destination is a stream wrapper, or when the moved file may be moved on, deleted or rewritten before you ask for its type. The object keeps what that call detected.
AttributesInterface
AttributesInterface is the only mutable part of the request object. It is intended for metadata your own code derives (authenticated user, resolved route parameters, locale, feature flags) and reads again further along the same call chain. Only code that receives the Request object holding it can see that data; see Attributes.
namespace OxPHP\Http;
interface AttributesInterface
{
public function get(string $key, mixed $default = null): mixed;
public function set(string $key, mixed $value): void;
public function has(string $key): bool;
public function remove(string $key): void;
public function all(): array;
}| Method | Description |
|---|---|
get(key, default) |
Returns the value for $key, or $default if absent |
set(key, value) |
Stores a value |
has(key) |
true if the key has been set |
remove(key) |
Removes the key |
all() |
All attributes as an associative array |
Exceptions
Calling oxphp_http_request() outside an active request context throws an exception from the OxPHP\Http\Exception namespace.
namespace OxPHP\Http\Exception;
class NoActiveRequestException extends \RuntimeException {}
class AsyncContextException extends NoActiveRequestException {}
class WorkerIdleException extends NoActiveRequestException {}| Exception | When thrown |
|---|---|
NoActiveRequestException |
No active HTTP request: CLI, MINIT, after shutdown, or a Fiber that outlives its request |
AsyncContextException |
Inside an oxphp_async() callback. Async workers run on separate threads with no request context |
WorkerIdleException |
Worker mode, between requests, while the worker is waiting for the next request |
AsyncContextException and WorkerIdleException both extend NoActiveRequestException, so catching the base class handles all cases.
<?php
try {
$request = oxphp_http_request();
} catch (\OxPHP\Http\Exception\AsyncContextException $e) {
// Inside oxphp_async() — no request context here
} catch (\OxPHP\Http\Exception\WorkerIdleException $e) {
// Worker is between requests — do not call oxphp_http_request() here
} catch (\OxPHP\Http\Exception\NoActiveRequestException $e) {
// Any other case with no active request
}In normal request-handling code, you do not need this try/catch. The exception guard is useful in bootstrap code that might run outside a request context.
SUPERGLOBALS_ENABLED
SUPERGLOBALS_ENABLED=true # default — full backward compatibility
SUPERGLOBALS_ENABLED=false # the $_SERVER request keys and $_GET are not builtBy default, OxPHP populates $_GET, $_POST, $_COOKIE, $_FILES, and $_SERVER as usual. The HTTP Object API is available alongside superglobals in this mode.
Setting SUPERGLOBALS_ENABLED=false skips the work of describing the request to PHP: the CGI and HTTP_* variables are not registered, and the query string is not handed over for PHP to parse. The effect is narrower than the name suggests. The request body and the Cookie header reach PHP either way, and PHP builds the arrays derived from them regardless of this setting:
| Feature | Behavior with SUPERGLOBALS_ENABLED=false |
|---|---|
oxphp_http_request() |
Available; every method answers, including query() and payload() |
php://input |
Available (it is a stream, not a superglobal) |
$_SESSION |
Available (managed by PHP's session module) |
header(), headers_list() |
Available (SAPI output functions) |
session_start(), session_*() |
Available (native PHP functions) |
$_POST, $_FILES, $_COOKIE |
Populated as usual. They are built from the body and the Cookie header, which this setting does not touch |
$_REQUEST |
Populated. PHP merges it as always (see Superglobals); only what $_GET would have contributed is missing |
$_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 / |
This setting therefore does not keep request data out of PHP's globals and does not stop a form body from being parsed. It skips only the per-request $_SERVER fold (the CGI and HTTP_* variables plus the process environment) and PHP's parse of the query string into $_GET.
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.
Use oxphp_superglobals_enabled() to check the current setting at runtime:
<?php
if (!oxphp_superglobals_enabled()) {
$method = oxphp_http_request()->method();
} else {
$method = $_SERVER['REQUEST_METHOD'];
}Worker Mode
In worker mode, oxphp_http_request() builds a new Request on each call rather than once per request. Call it inside the handler and pass the object where it is needed, as the example below does.
<?php
// worker.php
require __DIR__ . '/vendor/autoload.php';
$app = new MyApp\Application();
oxphp_worker(function () use ($app) {
$request = oxphp_http_request();
$app->handle($request);
});Do not keep an object past the handler that built it. An object held across requests is not invalidated and is never reported as stale. method(), path(), header() and the other uncached readers return data from whichever request the worker thread is serving when you call them. The cached values (the query() array, the parsed payload(), the attributes container) hold whatever each reader captured on its first call; a reader first called during the current request captures this request's data. Nothing on a Request is cleared when the next request begins. What the soft reset between requests does cover is listed under What resets and what persists.
IDE support
Install the stub package to get autocompletion and type checking in PhpStorm, VS Code, or any LSP-aware editor:
composer require --dev oxphp/stubsThe stubs package provides:
oxphp-stubs/
├── OxPHP/Http/
│ ├── RequestInterface.php
│ ├── SessionInterface.php
│ ├── UploadedFileInterface.php
│ ├── AttributesInterface.php
│ ├── Request.php
│ ├── Session.php
│ ├── UploadedFile.php
│ ├── Attributes.php
│ └── Exception/
│ ├── NoActiveRequestException.php
│ ├── AsyncContextException.php
│ └── WorkerIdleException.php
└── functions.phpNo runtime dependency is added. The package is require-dev only.
Examples
Traditional mode
<?php
$request = oxphp_http_request();
$method = $request->method(); // "GET"
$path = $request->path(); // "/api/articles"
$page = $request->query('page', 1); // "2" or 1 (default)
// Authorization header
if (!$request->hasHeader('Authorization')) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$token = $request->header('Authorization');
// Structured logging with request metadata
error_log(sprintf(
'[%s] %s %s from %s',
oxphp_request_id(),
$method,
$path,
$request->ip()
));POST with JSON body
<?php
$request = oxphp_http_request();
if (!$request->isMethod('POST')) {
http_response_code(405);
exit;
}
$email = $request->payload('email');
$password = $request->payload('password');
if (!$email || !$password) {
http_response_code(400);
echo json_encode(['error' => 'email and password are required']);
exit;
}
// payload() handles JSON, form-urlencoded, and multipart
// No manual json_decode() or $_POST check needed
$user = authenticate($email, $password);
header('Content-Type: application/json');
echo json_encode(['token' => $user->generateToken()]);Middleware attributes
<?php
// auth-middleware.php
function authenticate_request(\OxPHP\Http\RequestInterface $request): void
{
$token = $request->header('Authorization');
if (!$token) {
http_response_code(401);
exit;
}
$user = verify_token(str_replace('Bearer ', '', $token));
if (!$user) {
http_response_code(403);
exit;
}
$request->attributes()->set('user', $user);
}
// route-handler.php
$request = oxphp_http_request();
authenticate_request($request);
$user = $request->attributes()->get('user');
echo json_encode(['id' => $user->id, 'name' => $user->name]);Worker Mode with session
<?php
// worker.php
require __DIR__ . '/vendor/autoload.php';
oxphp_worker(function () {
$request = oxphp_http_request();
if ($request->path() === '/login' && $request->isMethod('POST')) {
$username = $request->payload('username');
$password = $request->payload('password');
if (verify_credentials($username, $password)) {
session_start();
$_SESSION['user'] = $username;
$_SESSION['authenticated'] = true;
session_write_close();
header('Location: /dashboard');
} else {
http_response_code(401);
echo 'Invalid credentials';
}
return;
}
if ($request->path() === '/dashboard') {
session_start();
$session = $request->session();
if (!$session || !$session->get('authenticated')) {
session_write_close();
header('Location: /login');
return;
}
echo 'Welcome, ' . htmlspecialchars($session->get('user'));
session_write_close();
}
});Calling session_write_close() on every exit path puts the write, and the release of whatever the save handler locked, at the point your application chooses. A session left open is still written and closed when its request ends, as long as no other request the worker is carrying is in it. One that another request is in stays open, with the save handler still holding whatever it locked, until the worker gives it back. A request is in a session if it opened it or was admitted while it was open, whether or not it ever calls session_start(). So a long-lived stream admitted in that window keeps the session open for as long as the stream runs, and the requests admitted meanwhile are handed it even after the request that opened it has returned.
Neither closing it yourself nor the close at the end of the request separates requests that overlap. A worker admits new requests whenever the one it is running suspends, and a session still open at a suspension point is handed to whichever request is admitted in that window. One closed with session_write_close() before that point leaves its id installed, for the request admitted in that window to adopt with its own session_start(). See Worker mode.
Prompts for your agent
These prompts assume an agent with the OxPHP documentation MCP server connected. The agent reads the pages it needs itself.
Move my code off superglobals and onto the OxPHP request API. Read the `php/request-api` page through the OxPHP documentation MCP server.
Find the places in this repository that read request data directly, and show me the rewrite for the ones where it changes behavior rather than just style.See also
- Superglobals: how OxPHP populates
$_SERVER,$_GET,$_POST,$_COOKIE, and$_FILES - PHP Functions: full reference for
oxphp_http_request(),oxphp_superglobals_enabled(), and all other built-in functions - Worker Mode: persistent PHP workers and the request lifecycle
- Configuration Reference:
SUPERGLOBALS_ENABLEDand other environment variables