API HTTP-запроса

OxPHP предоставляет объектно-ориентированный API для доступа к данным HTTP-запроса. Вместо чтения $_GET, $_POST, $_COOKIE, $_FILES и $_SERVER вы вызываете методы объекта Request, который возвращает ровно то, что вы запрашиваете, — не больше и не меньше.

Содержание

Обзор

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
<?php $request = oxphp_http_request();

Вызывайте oxphp_http_request() в любом месте скрипта, выполняющегося в рамках активного HTTP-запроса, в том числе внутри колбэка oxphp_worker():

php
<?php oxphp_worker(function () { $request = oxphp_http_request(); $method = $request->method(); // ... });

Методы RequestInterface

URI и метод

php
$request->method(): string

Возвращает HTTP-метод в верхнем регистре: "GET", "POST", "PUT", "PATCH", "DELETE" и т. д.

php
$request->isMethod(string $method): bool

Проверка метода без учёта регистра.

php
$request->path(): string

Путь URI без строки запроса: "/users/42".

php
$request->fullUri(): string

Полный URI, включая схему, хост, необязательный нестандартный порт, путь и строку запроса: "https://example.com:8080/users/42?page=2". Стандартные порты (80 для HTTP, 443 для HTTPS) опускаются.

php
$request->scheme(): string

"https" или "http".

php
$request->isSecure(): bool

true, если схема — "https".

php
$request->host(): string

Имя хоста из заголовка Host. Возвращает пустую строку, если заголовок отсутствует (запросы HTTP/1.0 без заголовка Host).

php
$request->port(): int

Порт из заголовка Host. Если он не указан явно, возвращает значение по умолчанию для схемы: 80 для HTTP, 443 для HTTPS.

За обратным прокси

scheme(), isSecure(), host() и port() учитывают X-Forwarded-Proto и X-Forwarded-Host, если удалённый узел (peer) входит в TRUSTED_PROXIES. Без доверенных прокси они отражают параметры прямого соединения.

php
$request->queryString(): ?string

Необработанная строка запроса без ведущего ?. Возвращает null, если строки запроса нет.

Протокол

php
$request->httpProtocol(): string

Полная строка протокола: "HTTP/1.1" или "HTTP/2".

php
$request->httpProtocolVersion(): string

Только номер версии: "1.1" или "2".

Параметры строки запроса

php
$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, пока это не исправлено.

php
// 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:

php
// 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:

php
// 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() останавливается.

Разобранное тело запроса

php
$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 — сюда
Тела форм — только для POST

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
<?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"]

Заголовки

php
$request->header(string $name, ?string $default = null): ?string

Возвращает необработанное значение заголовка. Имена заголовков нечувствительны к регистру. Для заголовков с несколькими значениями (Accept, X-Forwarded-For) вся строка заголовка возвращается как единая строка. Разбор — на вашей ответственности.

php
$request->hasHeader(string $name): bool

Возвращает true, если указанный заголовок присутствует.

php
$request->headers(): array

Возвращает все заголовки в виде ассоциативного массива. Каждый ключ — это имя заголовка в том виде, в каком оно получено (без нормализации), а каждое значение — необработанная строка заголовка.

php
<?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

php
$request->cookie(string $name, ?string $default = null): ?string

Возвращает значение одной cookie или $default, если cookie отсутствует. Cookie, отправленная пустой (Cookie: a=), читается как ''; только отсутствующая cookie даёт $default.

php
$request->cookies(): array

Возвращает все cookies в виде ассоциативного массива пар имя-значение.

Декодирование

Cookies декодируются иначе, чем параметры строки запроса, — вслед за тем, что PHP делает при построении $_COOKIE: значения процентно декодируются, но + остаётся буквальным +, а имена не декодируются вовсе.

php
// 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
<?php $theme = $request->cookie('theme', 'light'); // "dark" or "light" $session = $request->cookie('session'); // null if absent $all = $request->cookies(); // ["theme" => "dark", ...]

Сырое тело запроса

php
$request->body(): string

Возвращает необработанные байты тела запроса. Это эквивалент file_get_contents('php://input') в OxPHP. В отличие от payload(), body() не кэшируется. Каждый вызов обращается к нижележащей структуре данных.

php
$request->contentType(): ?string

Возвращает значение заголовка Content-Type или null, если он отсутствует.

php
<?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() независимы. Вы можете вызвать оба в рамках одного запроса.

Загрузка файлов

php
$request->file(string $name): ?UploadedFileInterface

Возвращает загруженный файл для указанного имени поля или null, если поле отсутствует. Для полей-массивов (name="photos[]") возвращает первый файл.

php
$request->files(?string $name = null): array
Вызов Возвращает
$request->files() Все загруженные файлы в виде плоского массива UploadedFileInterface
$request->files('photos') Все файлы для поля photos (поддерживает name="photos[]")

Оба при каждом вызове строят новые объекты UploadedFileInterface. Два вызова file('avatar') возвращают два объекта для одного и того же загруженного файла, и тип, который один из них определил через type(), другому неизвестен. Сохраните полученный объект, если он нужен вам больше одного раза, как это делает пример ниже.

php
<?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())); } }

Клиент

php
$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. См. Доверенные прокси.

Тайминги

php
$request->startTime(bool $asFloat = false): int|float

Возвращает Unix-таймстамп момента получения этого запроса.

Вызов Возвращает
$request->startTime() Целое число секунд: 1711234567
$request->startTime(true) Число с плавающей точкой с субсекундной точностью: 1711234567.3412
php
<?php $elapsed = microtime(true) - $request->startTime(true); error_log(sprintf("Request took %.3fs so far", $elapsed));

Атрибуты

php
$request->attributes(): AttributesInterface

Возвращает изменяемый контейнер атрибутов, принадлежащий этому объекту Request. Используйте атрибуты, чтобы передавать данные из middleware в обработчики маршрутов и в любой другой код, получающий тот же объект, вместо глобальной переменной.

php
<?php // In authentication middleware $request->attributes()->set('user', $authenticatedUser); // In the route handler $user = $request->attributes()->get('user');
Контейнер принадлежит объекту, а не запросу

oxphp_http_request() при каждом вызове возвращает новый Request, и контейнер, полученный из него, — новый и пустой. Запись, сделанная через один вызов, не читается через другой: get() возвращает значение по умолчанию, и ни предупреждения, ни записи в лог не появляется.

php
<?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() и получает свой собственный.

Сессия

php
$request->session(): ?SessionInterface

Возвращает представление $_SESSION только для чтения. Возвращает null, если session_start() не был вызван. Управление сессией (запуск, сохранение, уничтожение, запись значений) выполняется стандартными функциями сессий PHP.

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 — это представление активной сессии только для чтения.

php
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 представляет один загруженный файл.

php
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
<?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" или выбрасывает исключение под обработчиком ошибок, превращающим предупреждения в исключения.
Tip

Вызывайте type() до moveTo(), когда место назначения — обёртка потока или когда перемещённый файл может быть перемещён дальше, удалён или перезаписан до того, как вы запросите его тип. Объект сохраняет то, что определил этот вызов.

AttributesInterface

AttributesInterface — единственная изменяемая часть объекта запроса. Он предназначен для метаданных, которые выводит ваш собственный код (аутентифицированный пользователь, разрешённые параметры маршрута, локаль, флаги функций) и затем снова читает дальше по той же цепочке вызовов. Эти данные видит только код, получивший объект Request, которому принадлежит контейнер; см. Атрибуты.

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

php
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
<?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

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

Маршрутизация по REQUEST_URI

Код, который маршрутизирует по $_SERVER['REQUEST_URI'], при этой настройке ничего там не найдёт и должен читать oxphp_http_request()->path(). В скрипте воркера это относится к обработчику. Начальная загрузка над ним видит заглушку /, описанную в таблице, которая тоже не является настоящим маршрутом.

Используйте oxphp_superglobals_enabled(), чтобы проверить текущую настройку во время выполнения:

php
<?php if (!oxphp_superglobals_enabled()) { $method = oxphp_http_request()->method(); } else { $method = $_SERVER['REQUEST_METHOD']; }

Режим воркеров

В режиме воркеров oxphp_http_request() строит новый Request при каждом вызове, а не один раз на запрос. Вызывайте его внутри обработчика и передавайте объект туда, где он нужен, как это делает пример ниже.

worker.php
<?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:

bash
composer require --dev oxphp/stubs

Пакет со стабами предоставляет:

text
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
<?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
<?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
<?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.php
<?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 — нужные страницы он прочитает сам.

prompt
Переведи мой код с суперглобальных переменных на API запроса OxPHP. Прочитай страницу `php/request-api` через MCP-сервер документации OxPHP. Найди в этом репозитории места, которые читают данные запроса напрямую, и покажи переписанный вариант для тех из них, где это меняет поведение, а не только стиль.

Смотрите также

Нашли ошибку? Сообщите →