HttpRequest вместо $_GET и $_SERVER: зачем и какие методы есть
Кирилл Новожилов
Автор
Содержание
Введение
Когда в коде на D7 встречается $_GET['id'] или $_SERVER['HTTP_HOST'], кажется, что это просто короткий путь к данным. На самом деле вы перепрыгиваете через целый слой ядра: тот самый, что фильтрует ввод, разбирает JSON-тело, расшифровывает cookie и помнит про urlrewrite. Иногда это сходит с рук, иногда оборачивается тонким багом, который ловишь полдня.
Дальше разберёмся, почему правильнее использовать Context::getCurrent()->getRequest() вместо суперглобалов, и какие методы HttpRequest закрывают повседневные задачи — от чтения параметров до заголовков и серверных переменных.
Что вы узнаете
- Что ядро успевает сделать с запросом ещё до того, как до него доберётся ваш код.
- Почему суперглобалы ломают контракт с ядром.
- Чем
HttpRequestотличается от «просто массива параметров». - Какие методы использовать для GET, POST, файлов, заголовков, cookie, JSON и серверных переменных.
Часть 1. Что происходит под капотом
Чтобы понять, почему $_GET и HttpRequest — это не одно и то же, полезно один раз посмотреть, что ядро делает с запросом на старте хита. Дальше будет много про «фильтры» и «расшифровку», и проще держать в голове реальную картину, чем верить на слово.
Откуда вообще берётся объект запроса
Точка сборки — HttpApplication::initializeContext(). Ядро читает суперглобалы и заворачивает их в объекты:
// bitrix/modules/main/lib/httpapplication.php
$server = new Server($params['server']); // обёртка над $_SERVER
$request = new HttpRequest(
$server,
$params['get'], // $_GET
$params['post'], // $_POST
$params['files'], // $_FILES
$params['cookie'] // $_COOKIE
);
$context->initialize($request, $response, $server, ['env' => $params['env']]);
После этого «правдой» становится объект HttpRequest. Именно поэтому ручная правка $_SERVER где-то в середине хита ни на что не влияет: ядро на него больше не смотрит.
Что делает конструктор
Самое интересное — внутри HttpRequest::__construct(). Он не просто раскладывает массивы по полям, а сразу выполняет несколько неочевидных вещей:
// bitrix/modules/main/lib/httprequest.php
public function __construct(Server $server, array $queryString, array $postData, array $files, array $cookies, array $jsonData = [])
{
$request = array_merge($queryString, $postData); // объединённый GET+POST
parent::__construct($server, $request);
$this->queryString = new Type\ParameterDictionary($queryString); // только GET
$this->postData = new Type\ParameterDictionary($postData); // только POST
$this->files = new Type\ParameterDictionary($files);
$this->cookiesRaw = new Type\ParameterDictionary($cookies); // как в браузере
$this->cookies = new Type\ParameterDictionary($this->prepareCookie($cookies)); // обработанные
$this->headers = $this->buildHttpHeaders($server); // нормализованные заголовки
$this->jsonData = new Type\ParameterDictionary($jsonData);
}
Обратите внимание на три момента. Во-первых, GET и POST хранятся и по отдельности, и в объединённом виде — отсюда потом возьмутся раздельные getQuery()/getPost() и универсальный get(). Во-вторых, cookie сразу существуют в двух версиях: сырые (cookiesRaw) и пропущенные через prepareCookie(). В-третьих, заголовки тут же собираются в объект HttpHeaders с нормализованными именами, а не остаются строками HTTP_* из $_SERVER.
Cookie: префикс и расшифровка
prepareCookie() — хороший пример того, сколько всего ядро берёт на себя. Он отсекает служебный префикс (по умолчанию BITRIX_SM_, но это опция cookie_name) и расшифровывает значения, которые Bitrix хранит зашифрованными:
// bitrix/modules/main/lib/httprequest.php
$cookiePrefix = Config\Option::get("main", "cookie_name", "BITRIX_SM") . "_";
$cookiesCrypter = new Web\CookiesCrypter();
// ...
$name = mb_substr($name, $cookiePrefixLength); // срезаем префикс
if ($cookiesCrypter->shouldDecrypt($name, $value)) {
$cookiesToDecrypt[$name] = $value; // и при необходимости расшифровываем
}
Поэтому getCookie('SOME_NAME') отдаёт уже расшифрованное значение и без префикса, а в $_COOKIE вы бы увидели сырое BITRIX_SM_SOME_NAME с зашифрованным содержимым. Если сырой вид всё-таки нужен — для этого есть getCookieRaw().
Фильтры: единая точка санитизации
А вот фильтрация навешивается уже после конструктора — через addFilter(). Метод прогоняет все источники сразу и аккуратно складывает результат обратно:
// main/lib/httprequest.php (сокращённо)
public function addFilter(Type\IRequestFilter $filter)
{
parent::addFilter($filter);
$filteredValues = $filter->filter([
'get' => $this->queryString->values,
'post' => $this->postData->values,
'files' => $this->files->values,
'cookie' => $this->cookiesRaw->values,
'json' => $this->jsonData->values,
]);
// ... записываем отфильтрованные значения обратно в каждый источник ...
// и пересобираем объединённый GET+POST
$this->setValuesNoDemand(array_merge($this->queryString->values, $this->postData->values));
// requestedPage пересчитается заново — URL мог измениться
$this->requestedPage = null;
$this->requestedPageDirectory = null;
}
Главный фильтр здесь — proactive-защита из модуля security (класс Bitrix\Security\Filter\Request). Именно он навешивается на запрос вызовом $this->getHttpRequest()->addFilter(...) и режет потенциально опасные конструкции.
IRequestFilter — интерфейс открытый.JSON и urlrewrite — по запросу
Две вещи ядро намеренно не делает автоматически на старте.
JSON-тело не парсится само: суперглобалы его не содержат в принципе, оно лежит в php://input. Разбор запускается явно — decodeJson() проверяет isJson(), читает поток и складывает результат в jsonData:
// bitrix/modules/main/lib/httprequest.php
public function decodeJson(): void
{
if ($this->isJson()) {
$json = Web\Json::decode(static::getInput()); // file_get_contents('php://input')
if (is_array($json)) {
$this->jsonData = new Type\ParameterDictionary($json);
}
}
}
А urlrewrite учитывается в момент, когда вы спрашиваете про текущий файл. getScriptFile() понимает, что физически отработал диспетчер роутинга, и подменяет его на реальный путь:
// bitrix/modules/main/lib/httprequest.php
public function getScriptFile()
{
$scriptName = $this->getScriptName();
if ($scriptName == "/bitrix/routing_index.php" || $scriptName == "/bitrix/urlrewrite.php" || $scriptName == "/404.php") {
if (($v = $this->server->get("REAL_FILE_PATH")) != null) {
$scriptName = $v; // настоящий файл после rewrite
}
}
return $scriptName;
}
Дальше разберём по пунктам, чем это оборачивается на практике.
Часть 2. Почему не суперглобалы
Фильтрация ввода
Как мы видели выше, фильтры навешиваются на запрос через addFilter(). Практический итог для вас простой: всё, что вы достаёте через $request->get('name') или $request['name'], уже прошло через эту цепочку (как минимум через проактивную-защиту). Суперглобалы же остаются ровно тем, что прислал клиент, — ядро их не трогает.
// С фильтрами ядра
$id = (int)$request->get('id');
// Сырое значение до фильтров — только если осознанно нужно
$raw = $request->getQueryList()->getRaw('title');
$_REQUEST — ловушка
С $_REQUEST отдельная история. Он сваливает в кучу GET, POST и cookie, причём приоритет источников зависит от request_order в php.ini. На практике это значит, что параметр из cookie может незаметно перебить GET — и вот вам классическая дыра в логике, а заодно подспорье для CSRF-сценариев. В HttpRequest такого не случится: источники честно разведены по getQuery(), getPost() и getCookie().
$_SERVER без контекста urlrewrite
Ядро переписывает URI — через старый urlrewrite.php и через новый роутинг. После этого реальный путь оседает в объекте Server (поле REAL_FILE_PATH), а getScriptFile() отдаёт тот PHP-файл, который действительно отработает. С $_SERVER['SCRIPT_NAME'] это совпадает далеко не всегда.
$script = $request->getScriptFile();
// /bitrix/routing_index.php → /local/routes/... или /news/index.php
Если ориентироваться только на $_SERVER, легко промахнуться при определении текущей страницы или каталога — особенно на ЧПУ.
JSON-тело, заголовки, cookie
REST и BX.ajax.runAction присылают данные как application/json. В суперглобалы это тело не попадает в принципе, и достать его помогает HttpRequest::decodeJson(), складывая результат в getJsonList().
С cookie похожая ситуация. Bitrix хранит их с префиксом BITRIX_SM_ и часть значений шифрует. getCookie() вернёт уже расшифрованное значение и без префикса, а если зачем-то понадобился сырой вид как в $_COOKIE — есть getCookieRaw().
Заголовки тоже приводятся к человеческому виду через HttpHeaders: пишете getHeader('X-Auth-Token') и не вспоминаете, что в $_SERVER это превратилось бы в HTTP_X_AUTH_TOKEN.
Тестируемость и единый API
Наконец, Context::getCurrent() — это точка входа для всего HTTP-хита, и в тестах или CLI её можно подменить. Суперглобалы так не умеют: это глобальное изменяемое состояние процесса, и в юнит-тесте вы с ним намучаетесь.
Всю длинную цепочку при этом писать необязательно:
use Bitrix\Main\Context;
$request = Context::getCurrent()->getRequest();
// то же, что Application::getInstance()->getContext()->getRequest()
Часть 3. Карта методов HttpRequest
Теперь к практике. Сам класс лежит в Bitrix\Main\HttpRequest (bitrix/modules/main/lib/httprequest.php), часть методов он подхватывает от родителя Bitrix\Main\Request. Ниже — карта по группам задач, чтобы не держать всё это в голове.
Параметры запроса
| Метод | Назначение |
|---|---|
get($name) |
GET + POST (объединённый словарь), с фильтрами |
getQuery($name) / getQueryList() |
Только GET |
getPost($name) / getPostList() |
Только POST |
getFile($name) / getFileList() |
Загрузки, структура как в $_FILES |
getJsonList() |
Тело JSON после decodeJson() |
toArray() |
Все GET+POST одним массивом |
ParameterDictionary у списков: getRaw($name), getValues(), isEmpty().
Заголовки и cookie
| Метод | Назначение |
|---|---|
getHeader($name) |
Один заголовок (имя без HTTP_) |
getHeaders() |
Объект HttpHeaders |
getCookie($name) / getCookieList() |
Cookie Bitrix, расшифрованные |
getCookieRaw($name) / getCookieRawList() |
Как пришло из браузера |
getCookiesMode() |
Режим постоянных cookie (Y/N) |
Метод, URI, окружение
| Метод | Назначение |
|---|---|
getRequestMethod() |
GET, POST, … |
isPost() |
Метод POST |
isAjaxRequest() |
X-Requested-With: XMLHttpRequest или HTTP_BX_AJAX |
isHttps() |
Порт 443, HTTPS, плюс https_request из конфига |
isAdminSection() |
/bitrix/admin/, updates, ADMIN_SECTION |
getRequestUri() |
Полный URI с query string |
getRequestedPage() |
Путь страницы (с учётом decode/normalize) |
getRequestedPageDirectory() |
Каталог с завершающим / |
getScriptFile() |
Файл после urlrewrite |
getScriptName() / getPhpSelf() |
Из Server |
getHttpHost() |
Хост без порта |
getDecodedUri() |
URI в кодировке сайта |
getRemoteAddress() |
IP клиента |
getUserAgent() |
User-Agent |
getAcceptedLanguages() |
Массив из Accept-Language |
getServerPort() |
Порт |
Для PUT и DELETE готового хелпера в ядре нет, так что сравнивайте напрямую: $request->getRequestMethod() === 'PUT'.
JSON и сырое тело
| Метод | Назначение |
|---|---|
isJson() |
Content-Type application/json или *+json |
decodeJson() |
Мягкий разбор тела в jsonData |
decodeJsonStrict() |
Строгий разбор, исключение при ошибке |
getInput() |
Статически: file_get_contents('php://input') |
Серверные переменные — через Server
Если нужна именно серверная переменная, не лезьте в $_SERVER руками. $request->getServer() отдаёт Bitrix\Main\Server — ту же обёртку над $_SERVER, но с фильтрами и удобными хелперами:
$server = Context::getCurrent()->getServer();
// или $request->getServer()
$server->get('REMOTE_ADDR');
$server->getDocumentRoot();
$server->getHttpHost();
$server->getRequestUri();
$server->getPersonalRoot(); // BX_PERSONAL_ROOT или /bitrix
$server->parseAuthRequest(); // Basic/Digest auth
Методы Server::rewriteUri() и transferUri() — это внутренняя кухня urlrewrite. Важно помнить, что если вы правите $_SERVER вручную, эти механизмы о ваших изменениях не узнают и контекст разъедется.
Системные GET-параметры Bitrix
Ещё одна мелочь, которая иногда выручает: HttpRequest::getSystemParameters() отдаёт белый список служебных ключей (sessid, logout, clear_cache и прочие). Удобно, когда нужно отделить параметры пользователя от того, что подмешивает само ядро.
Часть 4. Практический минимум
Собственно, как это выглядит в живом коде — типовая обработка запроса, где есть и query-параметр, и POST, и заголовок, и JSON-тело:
<?php declare(strict_types=1);
use Bitrix\Main\Context;
$request = Context::getCurrent()->getRequest();
$request->decodeJson();
$id = (int)$request->getQuery('id');
$email = (string)$request->getPost('email');
$token = (string)$request->getHeader('X-Auth-Token');
$payload = $request->getJsonList()->get('filter');
if ($request->isPost() && $request->isAjaxRequest()) {
// ...
}
if ($request->isAdminSection()) {
// ...
}
В контроллерах D7 параметры экшена нередко autowire-ятся из запроса автоматически, так что там getRequest() руками дёргать почти не приходится. А вот в сервисах, агентах и компонентах явный вызов остаётся нормой — и это нормально.
Антипаттерны
| Нельзя | Почему |
|---|---|
$_REQUEST['id'] |
Смешение источников, непредсказуемый приоритет |
$_GET в сервисе модуля |
Нет фильтров ядра, сложно тестировать |
$_SERVER['HTTP_X_...'] напрямую |
Нет нормализации имён заголовков |
Читать JSON из $_POST |
Тело JSON туда не попадает |
Править $_SERVER вручную |
Расходится с Context и urlrewrite |
Итог
В новом коде внутри /local/ суперглобалам на входе делать нечего — единственное исключение — bootstrap, пока ядро ещё не подняло контекст. Во всех остальных местах привыкайте начинать с Context::getCurrent()->getRequest(), и половина тонких багов с источниками данных отпадёт сама собой.
Комментарии (0)
Пока нет ни одного комментария. Будьте первым!
Похожие статьи