API HTTP-запроса
OxPHP предоставляет объектно-ориентированный API для доступа к данным HTTP-запроса. Вместо чтения $_GET, $_POST, $_COOKIE, $_FILES и $_SERVER вы вызываете методы объекта Request, который возвращает ровно то, что вы запрашиваете, — не больше и не меньше.
Содержание
- Обзор
- Получение объекта Request
- Методы RequestInterface
- SessionInterface
- UploadedFileInterface
- AttributesInterface
- Исключения
- SUPERGLOBALS_ENABLED
- Режим воркеров
- Поддержка в IDE
- Примеры
Обзор
oxphp_http_request() возвращает прокси только для чтения к данным HTTP-запроса, хранящимся в текущем потоке воркера. Данные загружаются лениво. Одиночный вызов метода вроде $request->header('Accept') обращается напрямую к структуре данных на стороне Rust и возвращает только это значение. Из вызовов, возвращающих массив целиком, query() и payload() хранят разобранный результат на том объекте, у которого их вызвали; headers(), cookies() и files() строят свой массив заново при каждом вызове. На уровне запроса ничего не хранится: каждый вызов oxphp_http_request() возвращает новый объект с новым набором этих кэшей.
Зачем использовать его вместо суперглобальных переменных?
- Разбор JSON-тела встроен.
$request->payload()без дополнительного кода возвращает декодированное телоapplication/jsonпри любом методе запроса, а также поля тела формыPOST(application/x-www-form-urlencodedилиmultipart/form-data). Что именно возвращается, описано в разделе Разобранное тело запроса. - Никаких опечаток в ключах массива. В
$request->method()труднее ошибиться, чем в$_SERVER['REQUEST_METHOD']. - Определение типа загруженных файлов.
$request->file('avatar')->type()возвращает MIME-тип, определённый по фактическому содержимому файла, а не по значению, переданному клиентом. - Тестируемость. Поскольку поведение задаётся интерфейсами, в модульных тестах можно внедрять mock-реализации.
- Суперглобальные переменные остаются доступны. Установка
SUPERGLOBALS_ENABLED=falseнеобязательна. Объектный API работает в любом случае.
Получение объекта Request
<?php
$request = oxphp_http_request();Вызывайте oxphp_http_request() в любом месте скрипта, выполняющегося в рамках активного HTTP-запроса, в том числе внутри колбэка oxphp_worker():
<?php
oxphp_worker(function () {
$request = oxphp_http_request();
$method = $request->method();
// ...
});Методы RequestInterface
URI и метод
$request->method(): stringВозвращает HTTP-метод в верхнем регистре: "GET", "POST", "PUT", "PATCH", "DELETE" и т. д.
$request->isMethod(string $method): boolПроверка метода без учёта регистра.
$request->path(): stringПуть URI без строки запроса: "/users/42".
$request->fullUri(): stringПолный URI, включая схему, хост, необязательный нестандартный порт, путь и строку запроса: "https://example.com:8080/users/42?page=2". Стандартные порты (80 для HTTP, 443 для HTTPS) опускаются.
$request->scheme(): string"https" или "http".
$request->isSecure(): booltrue, если схема — "https".
$request->host(): stringИмя хоста из заголовка Host. Возвращает пустую строку, если заголовок отсутствует (запросы HTTP/1.0 без заголовка Host).
$request->port(): intПорт из заголовка Host. Если он не указан явно, возвращает значение по умолчанию для схемы: 80 для HTTP, 443 для HTTPS.
scheme(), isSecure(), host() и port() учитывают X-Forwarded-Proto и X-Forwarded-Host, если удалённый узел (peer) входит в TRUSTED_PROXIES. Без доверенных прокси они отражают параметры прямого соединения.
$request->queryString(): ?stringНеобработанная строка запроса без ведущего ?. Возвращает null, если строки запроса нет.
Протокол
$request->httpProtocol(): stringПолная строка протокола: "HTTP/1.1" или "HTTP/2".
$request->httpProtocolVersion(): stringТолько номер версии: "1.1" или "2".
Параметры строки запроса
$request->query(?string $key = null, mixed $default = null): mixedДоступ к параметрам строки запроса.
| Вызов | Возвращает |
|---|---|
$request->query() |
Все параметры в виде плоского массива пар имя-значение |
$request->query('page') |
Значение page или null, если отсутствует |
$request->query('page', 1) |
Значение page или 1, если отсутствует |
Синтаксис с квадратными скобками пока не разворачивается во вложенные массивы. ?tags[]=php&tags[]=async сейчас даёт плоскую запись с именем tags[], а не массив tags, и повторённое имя разрешается в первое вхождение через query('name'), тогда как query() сохраняет последнее. Для массивов параметров используйте $_GET, пока это не исправлено.
// Request: GET /search?q=oxphp&tags[]=php&tags[]=async
$q = $request->query('q'); // "oxphp"
$tags = $_GET['tags']; // ["php", "async"]Найденные значения всегда являются строками. $default возвращается как есть, если ключ отсутствует. Параметр, отправленный без значения (?flag), и параметр, отправленный пустым (?flag=), оба читаются как '' — только отсутствующий параметр даёт null.
Декодирование
Имена и значения процентно декодируются, а + читается как пробел — те же правила, которые PHP применяет при построении $_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"Значения — байтовые строки, поэтому escape-последовательность, кодирующая что-то, что не является корректным UTF-8, сохраняется нетронутой — подписанный токен или бинарный идентификатор всё ещё можно сверить с байтами в том виде, в каком они были отправлены. queryString() остаётся способом прочитать строку запроса ровно такой, какой она пришла, без декодирования.
Имена сообщаются так, как их отправил клиент, тогда как $_GET сообщает их так, как их регистрирует PHP. PHP переписывает ' ', '.' и '[' в именах суперглобальных переменных, поэтому ?a.b=1 попадает в $_GET как a_b:
// Request: GET /?a.b=1
$_GET['a_b']; // "1"
$request->query('a.b'); // "1"Имя также сохраняется целиком там, где PHP его обрезал бы. PHP обрабатывает имена суперглобальных переменных как C-строки, поэтому ?a%00b=1 превращается в $_GET['a'], тогда как query() сохраняет a\0b. Не воспринимайте это как повод валидировать через один API, а читать через другой: усечение позволяет одному параметру приземлиться на ключ другого, так что ?a=1&a%00x=2 даёт $_GET['a'] === '2', тогда как query('a') === '1', — и два API расходятся в значении, а не только в наборе имён.
Разбирается не более 1000 параметров. PHP применяет собственный лимит, max_input_vars, который по умолчанию равен тем же 1000, но настраивается — поднимите его, и $_GET будет содержать параметры за той точкой, где query() останавливается.
Разобранное тело запроса
$request->payload(?string $key = null, mixed $default = null): mixedВозвращает разобранное тело запроса. Тело разбирается в соответствии с заголовком Content-Type:
| Content-Type | Возвращает |
|---|---|
application/x-www-form-urlencoded |
Ассоциативный массив значений полей для POST-запроса; пустой массив при любом другом методе |
multipart/form-data |
Ассоциативный массив значений текстовых полей для POST-запроса; пустой массив при любом другом методе |
application/json |
Декодированное значение: массив для объекта или массива, иначе сам скаляр (string, int, float, bool); null при некорректном JSON и для тела, состоящего из литерала null |
| Любое другое значение | null. Сопоставление идёт по префиксу, поэтому всё, что начинается с application/json (например, application/json-patch+json), попадает в строку JSON выше, а text/json и application/vnd.api+json — сюда |
JSON-тело декодируется при любом методе запроса. Тело формы — нет: две строки для форм читают то, во что тело разобрал PHP, а PHP делает это только для POST, поэтому payload() на PUT или PATCH с полями формы возвращает пустой массив. Отправляйте такие тела в JSON или читайте их через body() и разбирайте сами.
Для обоих типов форм payload() возвращает поля в том виде, в каком PHP разобрал их из тела. Последующая запись в $_POST меняет только копию массива в скрипте, и payload() её не видит.
Разобранный результат кэшируется при первом вызове и переиспользуется последующими вызовами на том же объекте. Каждый вызов oxphp_http_request() возвращает отдельный объект со своим кэшем, поэтому второй объект строит результат заново.
| Вызов | Возвращает |
|---|---|
$request->payload() |
Всё разобранное тело запроса |
$request->payload(null, []) |
Всё разобранное тело запроса или [], если его нет: пустое тело, Content-Type из последней строки таблицы выше, некорректный JSON или литерал JSON null |
$request->payload('email') |
Значение одного поля или null, если отсутствует |
$request->payload('email', '') |
Значение одного поля или '', если отсутствует |
Поиск по ключу работает только в payload-массиве. У JSON-тела, декодированного в скаляр, ключей нет, поэтому payload('anything') возвращает значение по умолчанию. Тело, которое декодируется в false или 0, возвращается как это значение; значение по умолчанию его не заменяет.
<?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"]Заголовки
$request->header(string $name, ?string $default = null): ?stringВозвращает необработанное значение заголовка. Имена заголовков нечувствительны к регистру. Для заголовков с несколькими значениями (Accept, X-Forwarded-For) вся строка заголовка возвращается как единая строка. Разбор — на вашей ответственности.
$request->hasHeader(string $name): boolВозвращает true, если указанный заголовок присутствует.
$request->headers(): arrayВозвращает все заголовки в виде ассоциативного массива. Каждый ключ — это имя заголовка в том виде, в каком оно получено (без нормализации), а каждое значение — необработанная строка заголовка.
<?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): ?stringВозвращает значение одной cookie или $default, если cookie отсутствует. Cookie, отправленная пустой (Cookie: a=), читается как ''; только отсутствующая cookie даёт $default.
$request->cookies(): arrayВозвращает все cookies в виде ассоциативного массива пар имя-значение.
Декодирование
Cookies декодируются иначе, чем параметры строки запроса, — вслед за тем, что PHP делает при построении $_COOKIE: значения процентно декодируются, но + остаётся буквальным +, а имена не декодируются вовсе.
// 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 sentИмена и здесь сообщаются в том виде, в каком отправлены. PHP переписывает ' ', '.' и '[' в имени каждой суперглобальной переменной, включая cookies, поэтому Cookie: a.b=1 попадает в $_COOKIE как a_b, тогда как cookie('a.b') и cookies()['a.b'] используют имя, отправленное клиентом.
Значения — байтовые строки, поэтому подписанная cookie или setcookie('sid', random_bytes(16)) совершает полный оборот ровно с теми байтами, с которыми была установлена.
<?php
$theme = $request->cookie('theme', 'light'); // "dark" or "light"
$session = $request->cookie('session'); // null if absent
$all = $request->cookies(); // ["theme" => "dark", ...]Сырое тело запроса
$request->body(): stringВозвращает необработанные байты тела запроса. Это эквивалент file_get_contents('php://input') в OxPHP. В отличие от payload(), body() не кэшируется. Каждый вызов обращается к нижележащей структуре данных.
$request->contentType(): ?stringВозвращает значение заголовка Content-Type или null, если он отсутствует.
<?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() и payload() независимы. Вы можете вызвать оба в рамках одного запроса.
Загрузка файлов
$request->file(string $name): ?UploadedFileInterfaceВозвращает загруженный файл для указанного имени поля или null, если поле отсутствует. Для полей-массивов (name="photos[]") возвращает первый файл.
$request->files(?string $name = null): array| Вызов | Возвращает |
|---|---|
$request->files() |
Все загруженные файлы в виде плоского массива UploadedFileInterface |
$request->files('photos') |
Все файлы для поля photos (поддерживает name="photos[]") |
Оба при каждом вызове строят новые объекты UploadedFileInterface. Два вызова file('avatar') возвращают два объекта для одного и того же загруженного файла, и тип, который один из них определил через type(), другому неизвестен. Сохраните полученный объект, если он нужен вам больше одного раза, как это делает пример ниже.
<?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()));
}
}Клиент
$request->ip(): stringВозвращает IP-адрес клиента. Когда TRUSTED_PROXIES настроен и узел, от которого пришёл запрос, входит в доверенный набор, это самый правый недоверенный адрес из X-Forwarded-For или Forwarded (RFC 7239). Когда TRUSTED_PROXIES называет cloudflare, а цепочка перенаправления показывает, что запрос пришёл с пограничного узла Cloudflare, это вместо этого адрес из CF-Connecting-IP. В противном случае это IP непосредственного узла — как правило, вашего балансировщика нагрузки, а не конечного клиента.
Необработанный заголовок X-Forwarded-For по-прежнему доступен через $request->header('X-Forwarded-For') для нетривиальных случаев, но разбирать его вручную редко получается правильно (крайний левый или крайний правый адрес, отсутствие проверки доверия по CIDR). Вместо этого настройте TRUSTED_PROXIES. См. Доверенные прокси.
Тайминги
$request->startTime(bool $asFloat = false): int|floatВозвращает Unix-таймстамп момента получения этого запроса.
| Вызов | Возвращает |
|---|---|
$request->startTime() |
Целое число секунд: 1711234567 |
$request->startTime(true) |
Число с плавающей точкой с субсекундной точностью: 1711234567.3412 |
<?php
$elapsed = microtime(true) - $request->startTime(true);
error_log(sprintf("Request took %.3fs so far", $elapsed));Атрибуты
$request->attributes(): AttributesInterfaceВозвращает изменяемый контейнер атрибутов, принадлежащий этому объекту Request. Используйте атрибуты, чтобы передавать данные из middleware в обработчики маршрутов и в любой другой код, получающий тот же объект, вместо глобальной переменной.
<?php
// In authentication middleware
$request->attributes()->set('user', $authenticatedUser);
// In the route handler
$user = $request->attributes()->get('user');oxphp_http_request() при каждом вызове возвращает новый Request, и контейнер, полученный из него, — новый и пустой. Запись, сделанная через один вызов, не читается через другой: get() возвращает значение по умолчанию, и ни предупреждения, ни записи в лог не появляется.
<?php
oxphp_http_request()->attributes()->set('tenant_id', $id);
oxphp_http_request()->attributes()->get('tenant_id'); // null — a second Request, a second containerЕсли передавать объект дальше, как в примере выше с $request, весь код, который его держит, читает и пишет один и тот же контейнер, включая файбер, которому вы его передали. Файбер, который сам вызывает oxphp_http_request(), получает собственный контейнер, как и любой другой вызывающий код. Поскольку файберы PHP кооперативны, два из них никогда не обращаются к контейнеру одновременно.
Сбрасывать что-либо между запросами не нужно. Контейнер освобождается вместе с объектом, который его держит, а обработчик следующего запроса вызывает oxphp_http_request() и получает свой собственный.
Сессия
$request->session(): ?SessionInterfaceВозвращает представление $_SESSION только для чтения. Возвращает null, если session_start() не был вызван. Управление сессией (запуск, сохранение, уничтожение, запись значений) выполняется стандартными функциями сессий PHP.
<?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 — это представление активной сессии только для чтения.
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;
}| Метод | Описание |
|---|---|
id() |
Идентификатор сессии |
name() |
Имя сессии (по умолчанию: "PHPSESSID") |
get(key, default) |
Одно значение из сессии или $default, если ключ отсутствует |
has(key) |
true, если ключ существует в $_SESSION |
all() |
Все данные сессии в виде массива |
Значения сессии отражают текущее состояние $_SESSION на момент вызова, а не состояние на момент первого вызова session().
UploadedFileInterface
UploadedFileInterface представляет один загруженный файл.
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;
}| Метод | Описание |
|---|---|
name() |
Исходное имя файла, отправленное клиентом |
clientType() |
MIME-тип, заявленный клиентом, — не доверяйте этому значению при принятии решений о безопасности |
type() |
MIME-тип, определённый по фактическому содержимому файла с помощью анализа магических байтов (mime_content_type() из ext/fileinfo). Возвращает "application/octet-stream", если тип не удаётся определить. Определяется при первом вызове и сохраняется на этом объекте (см. Определение типа). |
size() |
Размер файла в байтах |
tmpPath() |
Путь к временному файлу на диске |
error() |
Одна из констант UPLOAD_ERR_* |
isValid() |
true, когда error() равно UPLOAD_ERR_OK |
moveTo(path) |
Перемещает файл в $path с помощью move_uploaded_file(), не определяя его тип. Возвращает false, если файл некорректен или перемещение не удалось. Что type() читает после перемещения, описано в разделе Определение типа. |
Всегда проверяйте isValid() перед использованием загруженного файла. Используйте type(), а не clientType(), при принятии решений, связанных с безопасностью:
<?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() читает файл и сохраняет результат на объекте; последующие вызовы на этом объекте возвращают его, не читая файл снова. Это относится и к "application/octet-stream" после неудачного определения, в том числе когда первый вызов выбросил исключение под обработчиком ошибок, превращающим предупреждения в исключения: последующие вызовы возвращают это значение без предупреждения. Объект из другого вызова file() или files() читает файл заново при своём первом вызове.
Без mime_content_type() (ext/fileinfo не загружено или функция указана в disable_functions) каждый вызов, который прочитал бы файл, выдаёт предупреждение и возвращает "application/octet-stream", не сохраняя его, или выбрасывает исключение под обработчиком ошибок, превращающим предупреждения в исключения. OxPHP также пишет об этом одну строку на процесс на уровне warn — в лог сервера или туда, куда указывает error_log, если он задан. Сами предупреждения логируются как любые другие предупреждения PHP.
moveTo() не определяет тип. Если type() не вызывался на объекте до перемещения, его первый вызов после успешного перемещения читает файл в месте назначения:
- Локальный путь — это перемещённый файл, найденный по абсолютному пути, по которому его поместило перемещение. Последующая смена рабочего каталога не имеет значения, в том числе та, которую PHP делает сам перед выполнением shutdown-функций и деструкторов.
- URL обёртки потока открывается заново через эту обёртку, и тип определяется по тому, что она возвращает. Там, где обёртка не может открыть URL на чтение (с предупреждением) или не может выполнить для него
stat(например,compress.zlib://; на PHP 8.4 — с предупреждением),type()возвращает"application/octet-stream"или выбрасывает исключение под обработчиком ошибок, превращающим предупреждения в исключения. - URL
data:никогда не декодируется.move_uploaded_file()переименовывает файл в него как в локальное имя файла, котороеtype()и читает, а если это не удаётся, копирует в URL, который не принимает записи: проходит только пустая загрузка, при этом ничего не записывается, иtype()не находит файла. - Другой объект для той же загрузки, у которого
type()не вызывался до перемещения (например, полученный заново черезfile()), не находит файла.mime_content_type()выдаёт предупреждение об отсутствующем файле, иtype()этого объекта возвращает"application/octet-stream"или выбрасывает исключение под обработчиком ошибок, превращающим предупреждения в исключения.
Вызывайте type() до moveTo(), когда место назначения — обёртка потока или когда перемещённый файл может быть перемещён дальше, удалён или перезаписан до того, как вы запросите его тип. Объект сохраняет то, что определил этот вызов.
AttributesInterface
AttributesInterface — единственная изменяемая часть объекта запроса. Он предназначен для метаданных, которые выводит ваш собственный код (аутентифицированный пользователь, разрешённые параметры маршрута, локаль, флаги функций) и затем снова читает дальше по той же цепочке вызовов. Эти данные видит только код, получивший объект Request, которому принадлежит контейнер; см. Атрибуты.
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;
}| Метод | Описание |
|---|---|
get(key, default) |
Возвращает значение для $key или $default, если отсутствует |
set(key, value) |
Сохраняет значение |
has(key) |
true, если ключ был установлен |
remove(key) |
Удаляет ключ |
all() |
Все атрибуты в виде ассоциативного массива |
Исключения
Вызов oxphp_http_request() вне контекста активного запроса выбрасывает исключение из пространства имён OxPHP\Http\Exception.
namespace OxPHP\Http\Exception;
class NoActiveRequestException extends \RuntimeException {}
class AsyncContextException extends NoActiveRequestException {}
class WorkerIdleException extends NoActiveRequestException {}| Исключение | Когда выбрасывается |
|---|---|
NoActiveRequestException |
Нет активного HTTP-запроса: CLI, MINIT, после завершения работы или файбер, переживший свой запрос |
AsyncContextException |
Внутри колбэка oxphp_async() — асинхронные воркеры выполняются в отдельных потоках без контекста запроса |
WorkerIdleException |
Режим воркеров, между запросами — воркер ожидает следующий запрос |
AsyncContextException и WorkerIdleException наследуются от NoActiveRequestException, поэтому перехват базового класса покрывает все случаи.
<?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
}В обычном коде обработки запросов этот try/catch не нужен. Защита от исключений полезна в коде инициализации (bootstrap), который может выполняться вне контекста запроса.
SUPERGLOBALS_ENABLED
SUPERGLOBALS_ENABLED=true # default — full backward compatibility
SUPERGLOBALS_ENABLED=false # the $_SERVER request keys and $_GET are not builtПо умолчанию OxPHP заполняет $_GET, $_POST, $_COOKIE, $_FILES и $_SERVER как обычно. В этом режиме объектный HTTP API доступен наряду с суперглобальными переменными.
Установка SUPERGLOBALS_ENABLED=false пропускает работу по описанию запроса для PHP: переменные CGI и HTTP_* не регистрируются, а строка запроса не передаётся PHP для разбора. Эффект уже, чем подсказывает название. Тело запроса и заголовок Cookie доходят до PHP в любом случае, и PHP строит производные от них массивы независимо от этой настройки:
| Возможность | Поведение при SUPERGLOBALS_ENABLED=false |
|---|---|
oxphp_http_request() |
Доступен; отвечает каждый метод, включая query() и payload() |
php://input |
Доступен (это поток, а не суперглобальная переменная) |
$_SESSION |
Доступен (управляется модулем сессий PHP) |
header(), headers_list() |
Доступны (функции вывода SAPI) |
session_start(), session_*() |
Доступны (нативные функции PHP) |
$_POST, $_FILES, $_COOKIE |
Заполняются как обычно. Они строятся из тела и заголовка Cookie, которых эта настройка не касается |
$_REQUEST |
Заполняется. PHP объединяет его как всегда (см. Суперглобальные переменные); не хватает только того, что внёс бы $_GET |
$_GET |
Пуст |
$_SERVER |
Во время запроса — только четыре ключа: REQUEST_TIME, REQUEST_TIME_FLOAT, argc и argv, все зарегистрированы самим PHP, а не сервером. REQUEST_METHOD, REQUEST_URI, ключи HTTP_* и окружение процесса отсутствуют. В режиме воркеров начальная загрузка устроена иначе: код выше oxphp_worker() выполняется до любого запроса и видит $_SERVER, построенный так, будто настройка включена, включая всё окружение процесса, плюс REQUEST_URI с заглушкой / |
Таким образом, эта настройка не убирает данные запроса из глобальных переменных PHP и не мешает разбору тела формы. Она пропускает только сборку $_SERVER для каждого запроса (переменные CGI и HTTP_* плюс окружение процесса) и разбор строки запроса в $_GET силами PHP.
Код, который маршрутизирует по $_SERVER['REQUEST_URI'], при этой настройке ничего там не найдёт и должен читать oxphp_http_request()->path(). В скрипте воркера это относится к обработчику. Начальная загрузка над ним видит заглушку /, описанную в таблице, которая тоже не является настоящим маршрутом.
Используйте oxphp_superglobals_enabled(), чтобы проверить текущую настройку во время выполнения:
<?php
if (!oxphp_superglobals_enabled()) {
$method = oxphp_http_request()->method();
} else {
$method = $_SERVER['REQUEST_METHOD'];
}Режим воркеров
В режиме воркеров oxphp_http_request() строит новый Request при каждом вызове, а не один раз на запрос. Вызывайте его внутри обработчика и передавайте объект туда, где он нужен, как это делает пример ниже.
<?php
// worker.php
require __DIR__ . '/vendor/autoload.php';
$app = new MyApp\Application();
oxphp_worker(function () use ($app) {
$request = oxphp_http_request();
$app->handle($request);
});Не храните объект дольше, чем живёт обработчик, который его построил. Объект, удерживаемый между запросами, не становится недействительным и никогда не сообщает, что устарел. method(), path(), header() и другие некэшируемые методы чтения возвращают данные того запроса, который поток воркера обслуживает в момент вызова. Кэшированные значения (массив query(), разобранный payload(), контейнер атрибутов) содержат то, что каждый метод захватил при первом вызове; метод, впервые вызванный во время текущего запроса, захватывает данные этого запроса. Ничто в Request не очищается при начале следующего запроса. Что именно покрывает мягкий сброс между запросами, перечислено в разделе Что сбрасывается, а что сохраняется.
Поддержка в IDE
Установите пакет со стабами, чтобы получить автодополнение и проверку типов в PhpStorm, VS Code или любом редакторе с поддержкой LSP:
composer require --dev oxphp/stubsПакет со стабами предоставляет:
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.phpНикакой зависимости времени выполнения не добавляется. Пакет предназначен только для require-dev.
Примеры
Традиционный режим
<?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 с JSON-телом
<?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
<?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]);Режим воркеров с сессией
<?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();
}
});Вызов session_write_close() на каждом пути выхода переносит запись и освобождение всего, что заблокировал обработчик сохранения, в ту точку, которую выбирает ваше приложение. Оставленная открытой сессия всё равно записывается и закрывается, когда заканчивается её запрос, если в ней нет ни одного другого запроса из тех, что несёт воркер. Сессия, в которой находится другой запрос, остаётся открытой, а обработчик сохранения продолжает удерживать всё, что заблокировал, пока воркер её не вернёт. Запрос находится в сессии, если открыл её или был принят, пока она была открыта, независимо от того, вызывает ли он когда-нибудь session_start(). Поэтому долгоживущий потоковый запрос, принятый в этот промежуток, держит сессию открытой всё время, пока он работает, а запросы, принятые тем временем, получают её даже после того, как открывший её запрос вернулся.
Ни ваше собственное закрытие, ни закрытие в конце запроса не разделяют перекрывающиеся запросы. Воркер принимает новые запросы всякий раз, когда выполняемый им запрос приостанавливается, и сессия, всё ещё открытая в точке приостановки, достаётся тому запросу, который был принят в этот промежуток. Сессия, закрытая через session_write_close() до этой точки, оставляет свой идентификатор установленным, и запрос, принятый в этот промежуток, подхватывает его собственным session_start(). См. Режим воркеров.
Промпты для вашего агента
Эти промпты рассчитаны на агента с подключённым MCP-сервером документации OxPHP — нужные страницы он прочитает сам.
Переведи мой код с суперглобальных переменных на API запроса OxPHP. Прочитай страницу `php/request-api` через MCP-сервер документации OxPHP.
Найди в этом репозитории места, которые читают данные запроса напрямую, и покажи переписанный вариант для тех из них, где это меняет поведение, а не только стиль.Смотрите также
- Суперглобальные переменные — как OxPHP заполняет
$_SERVER,$_GET,$_POST,$_COOKIEи$_FILES - Функции PHP — полный справочник по
oxphp_http_request(),oxphp_superglobals_enabled()и всем остальным встроенным функциям - Режим воркеров — постоянные воркеры PHP и жизненный цикл запроса
- Справочник конфигурации —
SUPERGLOBALS_ENABLEDи другие переменные окружения