Обработка GET-запроса в строке URL средствами PHP
Нередко возникает задача обработки GET-запроса, содержащегося в строке URL: например, получение подстроки, отвечающей за этот запрос, замена параметров запроса, удаление его из URL и др. В данной статье читателю предлагаются простые функции, решающие некоторые из таких задач.
Вообще говоря, любая строка URL в терминах PCRE 1 может быть нестрого описана вот таким регулярным выражением:
Приведенное выше регулярное выражение описывает GET-параметры как фрагмент строки адреса, начинающийся со знака вопроса и продолжающийся до конца строки или до первой решётки (#), если таковая имеется 2; про якоря (anchors) в адресах часто забывают — именно они объявляются с помощью решётки.
Вооружившись функциями для работы с регулярными выражениями, несложно проводить с GET-параметрами различные манипуляции.
Получение строки GET-запроса.
Для начала поставим самую простую задачу — получить часть URL, содержащую GET-параметры.
preg_match('/^([^?]+)(\?.*?)?(#.*)?$/', $url, $matches);
$gp = (isset($matches[2])) ? $matches[2] : '';
return $gp;
}
Не стоит забывать, что адрес может вовсе не содержать никакого GET-запроса, и массив совпадений может не иметь второго элемента 3.
Исключение GET-запроса из URL.
Иногда нужно получить URL без GET-параметров (например, при перенаправлении запросов с помощью mod_rewrite зачастую требуется проводить анализ URL, чтобы сформировать ответ клиенту; нередко для анализа нужна только статическая часть URL, а часть, где передается GET-запрос, не нужна и даже мешает). Эта операция занимает фактически одну строку, но, чтобы не писать каждый раз однотипный код, удобно вынести его в функцию 4:
return preg_replace('/^([^?]+)(\?.*?)?(#.*)?$/', '$1$3', $url);
}
Замена содержимого GET-параметров.
Нередко требуется, имея адрес страницы, добавить к нему какой-нибудь GET-запрос. Как правило, это делается простым дописыванием к адресу строки вида ?имя_переменной=значение. Однако не всегда бывает заранее известно, не содержит ли уже адрес какого-нибудь GET-запроса — если вдруг содержит, то простое дописывание приведет к адресу, содержащему два знака вопроса (вида '?имя1=значение1?имя2=значение2') и потому некорректному, что приводит к необходимости на этот счет строку адреса специально проверять.
Еще более сложная ситуация возникает в случае, когда в URL уже может находиться нужный к передаче GET-параметр (т.е. с тем же именем) — в этом случае нужно даже не просто корректно дописать адрес, а найти там нужную переменную и переписать её значение. Например, было
, а нужно
Все эти задачи может решить функция sgp():
echo sgp($url, 'page', 4); // выведет '/article.php?view=flat&page=4&mode=1#note_1'
echo sgp($url, 'view', 'tree'); // выведет '/article.php?view=tree&page=3&mode=1#note_1'
echo sgp($url, 'view', ''); // выведет '/article.php&page=3&mode=1#note_1'
За один вызов функции можно менять и несколько переменных:
sgp($url,
array('page', 'view'),
array(4, 'normal')
); // ..?view=normal&page=4&mode=1#note_1
sgp(
$url,
array('page', 'view'),
NULL // или просто опустить третий аргумент
); // ..?mode=1#note_1
В примерах для краткости изложения приведены относительные имена. Функция, разумеется, может работать и с абсолютными адресами, подчиняющимися стандарту URI. Код функции достаточно прост:
{
if (is_array($varname)) {
foreach ($varname as $i => $n) {
$v = (is_array($value))
? ( isset($value[$i]) ? $value[$i] : NULL )
: $value;
$url = sgp($url, $n, $v);
}
return $url;
}
preg_match('/^([^?]+)(\?.*?)?(#.*)?$/', $url, $matches);
$gp = (isset($matches[2])) ? $matches[2] : ''; // GET-parameters
if (!$gp) return $url;
$pattern = "/([?&])$varname=.*?(?=&|#|\z)/";
if (preg_match($pattern, $gp)) {
$substitution = ($value !== '') ? "\${1}$varname=" . preg_quote($value) : '';
$newgp = preg_replace($pattern, $substitution, $gp); // new GET-parameters
$newgp = preg_replace('/^&/', '?', $newgp);
}
else {
$s = ($gp) ? '&' : '?';
$newgp = $gp.$s.$varname.'='.$value;
}
$anchor = (isset($matches[3])) ? $matches[3] : '';
$newurl = $matches[1].$newgp.$anchor;
return $newurl;
}
Ниже в статье приводится также версия функции без интенсивного использования регулярных выражений.
Для начала получаем GET-параметры адреса (код, аналогичный функции ggp()):
$gp = (isset($matches[2])) ? $matches[2] : ''; // GET-parameters
Если GET-параметры отсутствуют — заменять негде, переданный адрес заведомо останется неизменным и его можно сразу вернуть, завершив работу функции:
Далее нужно сконструировать регулярное выражение, описывающее указание в адресе на данную переменную GET-запроса. Это регулярное выражение должно ловить строку вида имя_переменной=значение, которой предшествует знак вопроса или амперсанд, и вслед за которой следует конец строки (самый, наверное, частый случай), амперсанд (если начинается объявление следующей GET-переменной) или же решётка (если адрес содержит якорь):
Далее логика программы такая: если функции передается новое значение переменной, то производится замена старого значения на новое (при этом используется фрагмент строки, захваченный первой подмаской — это будет знак вопроса или амперсанд). Если же новое значение переменной — ни что иное, как пустая строка, то следует совсем исключить упоминание об этой переменной из строки запроса (т.е. получить адрес даже не вида /adress.php?v1=&v2=asdf, а просто /adress.php?v2=asdf). Такое поведение не является полностью строгим, поскольку может приводить к изменению количества передаваемых параметров GET-запроса, однако, автор считает такой вариант работы наиболее удобным, т.к. при этом происходит также очищение запроса от пустых переменных, которые в большинстве случаев являются просто мусором:
$newgp = preg_replace($pattern, $substitution, $gp); // new GET-parameters
Нужно не забывать и о ситуации, в которой переменная запроса, которую решили убрать, следовала в запросе первой. В таком случае из адреса вида /adress.php?v1=&v2=asdf&v3=1 получится некорректный адрес: /adress.php&v2=asdf&v3=1. В этом адресе первый амперсанд нужно заменить на знак вопроса, а поскольку располагаем мы не только целым адресом, но и отдельно строкой его GET-запроса (в примере — &v2=asdf&v3=1), сделать это особенно легко:
Если оказалось, что URL не содержит упоминания данной переменной, нужно эту переменную в него дописать, причем сделать это корректно: если URL вовсе не содержит GET-запроса (в этом случае переменная $gp будет содержать пустую строку), декларация переменной должна начинаться со знака вопроса, в противном случае — с амперсанда:
Далее окончательно формируем обновленную строку GET-запроса:
Обрабатываем возможное наличие якоря:
И, наконец, конструируем конечный адрес полностью, дописывая статическую часть адреса страницы и якорь, после чего возвращаем полученный результат:
return $newurl;
Следует отметить, что эту задачу можно решить и без интенсивного использования регулярных выражений:
$url,
$varname,
$value = NULL, // если NULL - убираем переменную совсем
$clean = TRUE // превращать ли ?one=&two=
) { // в ?one&two (так адрес красивее)
// Версия функции "substitue get parameter" без регулярных выражений
if (is_array($varname)) {
foreach ($varname as $i => $n) {
$v = (is_array($value))
? ( isset($value[$i]) ? $value[$i] : NULL )
: $value;
$url = sgp($url, $n, $v, $clean);
}
return $url;
}
$urlinfo = parse_url($url);
$get = (isset($urlinfo['query']))
? $urlinfo['query']
: '';
parse_str($get, $vars);
if (!is_null($value)) // одновременно переписываем переменную
$vars[$varname] = $value; // либо добавляем новую
else
unset($vars[$varname]); // убираем переменную совсем
$new_get = http_build_query($vars);
if ($clean)
$new_get = preg_replace( // str_replace() выигрывает
'/=(?=&|\z)/', // в данном случае
'', // всего на 20%
$new_get
);
$result_url = (isset($urlinfo['scheme']) ? "$urlinfo[scheme]://" : '')
. (isset($urlinfo['host']) ? "$urlinfo[host]" : '')
. (isset($urlinfo['path']) ? "$urlinfo[path]" : '')
. ( ($new_get) ? "?$new_get" : '')
. (isset($urlinfo['fragment']) ? "#$urlinfo[fragment]" : '')
;
return $result_url;
}
Версия без регулярных выражений работает где-то на треть быстрее, однако эта разница не чувствуется, поскольку общее время работы очень мало (1000 итераций выполняются за сотые доли секунды).
1. Работа с PCRE-совместимыми регулярными выражениями описана на соответствующей странице руководства по PHP.
2. Более строгое регулярное выражение, описывающее URL, можно найти в стандарте, описывающем URI (и, соответственно, URL как его частный случай), однако, в данном случае работать оно будет аналогично приводимому в данной статье: модификатор ? в первой подмаске делает квантификатор + нежадным, поэтому совпадение с первой подмаской продолжается вплоть до первого знака вопроса, после чего задействуется уже вторая подмаска. По той же причине совпадение со второй подмаской прекращается, как только встречается решётка, в противном случае продолжается до конца строки.
3. С переменным количеством подмасок приходится иметь дело довольно-таки часто. В регулярном выражении /^([^?]+)(\?.*?)?(#.*)?$/ гарантировано совпадает только первая подмаска. Вторая же может совсем отсутствовать в случае статического (без GET-запроса) адреса без якоря - именно для описания такого случая и служит код
В случае статического адреса с якорем обязана совпасть третья подмаска, поэтому вторая будет обязательно пронумерована, хотя и будет содежать пустую строку.
4. Вообще говоря, если положиться на корректность URL (т.е. допустить в т.ч., что знак вопроса встречается в адресе только один раз и решётка, если присутствует, следует за ним, а не перед ним), то можно использовать более простой код:
return preg_replace('/\?.*?(?=#|\z)/', '', $url);
// конструкция вида (?=что_то) - т.н. "заглядывание назад" - проверка последующего текста на совпадение со строкой 'что_то'
// данная строка совпадет, если за ней следует либо решётка, либо конец данных, обозначаемый служебной последовательностью \z
}
В принципе, уязвимость такого кода достаточно низкая: GET-параметры он успешно уберет, просто может затронуть находящуюся за решёткой-якорем часть строки, если она будет иметь вид ?что_то#что_то_еще?еще_что_то#и_т_д.... В большинстве случаев эта особенность поведения не имеет никакого значения, однако, о ней лучше знать.
© Все права на данную статью принадлежат порталу webew.ru. Перепечатка в интернет-изданиях разрешается только с указанием автора и прямой ссылки на оригинальную статью. Перепечатка в печатных изданиях допускается только с разрешения редакции.