Особенности http_build_query в PHP

Казалось бы http_build_query — простая функция, однако, имеет некоторые особенности. Нельзя однозначно сказать что это баг, скорее просто недокументированная фича, которую стоит учитывать при разработке.

Допустим, есть массив [‘page’ => 2, ‘hide’ => null]. Что же вернёт http_build_query в результате? Это будет просто page=2, аргумент hide был отброшен, т.к. он имеет значение null! Что будет, если передать массив, где все значения будут null? Функция вернёт строку нулевой длины.

Взаимосвязь parse_str и http_build_query

В документации PHP по функции http_build_query есть отсылка к другой функции — parse_str, которая делает обратное действие: возвращает массив [‘page’ => ‘2’, ‘hide’ => »] на основе строк ‘page=2&hide’ или ‘page=2&hide=’. Если этот массив прогнать обратно через http_build_query, то результат получится ‘page=2&hide=’, что для некоторых ситуаций будет детерминировано.

Влияние http_build_query на фреймворк Laravel

Однако, некоторые фреймворки, например, Laravel и, скорее всего Symfony, строку с параметрами ‘page=2&hide’ или ‘page=2&hide=’  парсят [‘page’ => ‘2’, ‘hide’ => null]. Самое странное, что обратно этот массив параметров они собирают с помощью всё той же http_build_query, в результате чего в урле теряется параметр hide.

Ещё одна ошибка, которая часть встречается в скриптах при формировании полного url. Вот пример из Laravel 5.6 из класса \Illuminate\Http\Request:

public function fullUrlWithQuery(array $query)
{
    $question = $this->getBaseUrl().$this->getPathInfo() == '/' ? '/?' : '?';
    return count($this->query()) > 0
        ? $this->url().$question.http_build_query(array_merge($this->query(), $query))
        : $this->fullUrl().$question.http_build_query($query);
}

Что вернёт данный метод, если в query все значения будут null? Что-то вроде: «http://127.0.0.1:8000/?». Как видите, на конце появился не нужный знак вопроса, т.к. после него нет ни одного параметра. Это происходит из-за того, что считается количество элементов в массиве вместе с null значениями.

Однако, соседний метод \Illuminate\Http\Request::fullUrl() вернёт корректное значение: «http://127.0.0.1:8000/?hide=»

Проблема потери параметров в http_build_query

Казалось бы, нет проблемы, теряется параметр без значения. Однако, для внешних систем, параметр без значения может быть значимым и его потеря приведёт к непредвиденному результату. Например, это может быть какая-нибудь JS-библиотека, для которой параметр без значения так же важен как и параметр со значением. Проблемы также могут возникнуть в аналитических системах, при интеграции с другими сайтами и системами.

Не все веб-приложения работают на PHP, не все соблюдают стандарты и спецификации, особенно в других странах. И иногда потеря get-параметра без значения может обернуться серьёзной ошибкой для бизнес-логики.

А что, если не баг, а фича?

Однако, у этой особенности работы http_build_query есть и приятная сторона. Можно достаточно просто удалять ненужные параметры из строки запроса таким способом:

$query = ['page' => 2, 'hide' => 6, 'user' => 4, 'section' => 22];
$except = ['hide' => null, 'user' => null];
$new = http_build_query(array_merge($query, $except) );

В результате чего получится строка: page=2&section=22.