webew
Войти » Регистрация
 
PHP

Обработка GET-запроса в строке URL средствами PHP

2 июля 2008, 11:55
Автор: Михаил Серов [1234ru]

Нередко возникает задача обработки GET-запроса, содержащегося в строке URL: например, получение подстроки, отвечающей за этот запрос, замена параметров запроса, удаление его из URL и др. В данной статье читателю предлагаются простые функции, решающие некоторые из таких задач.

Вообще говоря, любая строка URL в терминах PCRE 1 может быть нестрого описана вот таким регулярным выражением:

/^([^?]+)(\?.*?)?(#.*)?$/

Приведенное выше регулярное выражение описывает GET-параметры как фрагмент строки адреса, начинающийся со знака вопроса и продолжающийся до конца строки или до первой решётки (#), если таковая имеется 2; про якоря (anchors) в адресах часто забывают — именно они объявляются с помощью решётки.

Вооружившись функциями для работы с регулярными выражениями, несложно проводить с GET-параметрами различные манипуляции.

Получение строки GET-запроса.

Для начала поставим самую простую задачу — получить часть URL, содержащую GET-параметры.

function ggp($url) { // get GET-parameters string
    preg_match('/^([^?]+)(\?.*?)?(#.*)?$/', $url, $matches);
    $gp = (isset($matches[2])) ? $matches[2] : '';
    return $gp;
}

Не стоит забывать, что адрес может вовсе не содержать никакого GET-запроса, и массив совпадений может не иметь второго элемента 3.

Исключение GET-запроса из URL.

Иногда нужно получить URL без GET-параметров (например, при перенаправлении запросов с помощью mod_rewrite зачастую требуется проводить анализ URL, чтобы сформировать ответ клиенту; нередко для анализа нужна только статическая часть URL, а часть, где передается GET-запрос, не нужна и даже мешает). Эта операция занимает фактически одну строку, но, чтобы не писать каждый раз однотипный код, удобно вынести его в функцию 4:

function rgp($url) { // remove GET-parameters from URL
    return preg_replace('/^([^?]+)(\?.*?)?(#.*)?$/', '$1$3', $url);
}

Замена содержимого GET-параметров.

Нередко требуется, имея адрес страницы, добавить к нему какой-нибудь GET-запрос. Как правило, это делается простым дописыванием к адресу строки вида ?имя_переменной=значение. Однако, не всегда бывает заранее известно, не содержит ли уже адрес какого-нибудь GET-запроса — если вдруг содержит, то простое дописывание приведет к адресу, содержащему два знака вопроса (вида '?имя1=значение1?имя2=значение2') и потому некорректному, что приводит к необходимости на этот счет строку адреса специально проверять.

Еще более сложная ситуация возникает в случае, когда в URL уже может находиться нужный к передаче GET-параметр (т.е. с тем же именем) — в этом случае нужно даже не просто корректно дописать адрес, а найти там нужную переменную и переписать её значение. Например, было

/article.php?view=flat&page=3&mode=1#note_1

, а нужно

/article.php?view=flat&page=4&mode=1#note_1

Все эти задачи может решить функция sgp():

$url = '/article.php?view=flat&page=3&mode=1#note_1';
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'

В примерах для краткости изложения приведены относительные имена. Функция, разумеется, может работать и с абсолютными адресами, подчиняющимися стандарту URI. Код функции достаточно прост:

function sgp($url, $varname, $value) // substitute get parameter
{
    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()):

preg_match('/^([^?]+)(\?.*?)?(#.*)?$/', $url, $matches);
$gp = (isset($matches[2])) ? $matches[2] : ''; // GET-parameters

Если GET-параметры отсутствуют — заменять негде, переданный адрес заведомо останется неизменным и его можно сразу вернуть, завершив работу функции:

if (!$gp) return $url;

Далее нужно сконструировать регулярное выражение, описывающее указание в адресе на данную переменную GET-запроса. Это регулярное выражение должно ловить строку вида имя_переменной=значение, которой предшествует знак вопроса или амперсанд, и вслед за которой следует конец строки (самый, наверное, частый случай), амперсанд (если начинается объявление следующей GET-переменной) или же решётка (если адрес содержит якорь):

$pattern = "/([?&])$varname=.*?(?=&|#|\z)/";

Далее логика программы такая: если функции передается новое значение переменной, то производится замена старого значения на новое (при этом используется фрагмент строки, захваченный первой подмаской — это будет знак вопроса или амперсанд). Если же новое значение переменной — ни что иное, как пустая строка, то следует совсем исключить упоминание об этой переменной из строки запроса (т.е. получить адрес даже не вида /adress.php?v1=&v2=asdf, а просто /adress.php?v2=asdf). Такое поведение не является полностью строгим, поскольку может приводить к изменению количества передаваемых параметров GET-запроса, однако, автор считает такой вариант работы наиболее удобным, т.к. при этом происходит также очищение запроса от пустых переменных, которые в большинстве случаев являются просто мусором:

$substitution = ($value !== '') ? "\${1}$varname=" . preg_quote($value) : '';
$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), сделать это особенно легко:

$newgp = preg_replace('/^&/', '?', $newgp);

Если оказалось, что URL не содержит упоминания данной переменной, нужно эту переменную в него дописать, причем сделать это корректно: если URL вовсе не содержит GET-запроса (в этом случае переменная $gp будет содержать пустую строку), декларация переменной должна начинаться со знака вопроса, в противном случае — с амперсанда:

$s = ($gp) ? '&' : '?';

Далее окончательно формируем обновленную строку GET-запроса:

$newgp = $gp.$s.$varname.'='.$value;

Обрабатываем возможное наличие якоря:

$anchor = (isset($matches[3])) ? $matches[3] : '';

И, наконец, конструируем конечный адрес полностью, дописывая статическую часть адреса страницы и якорь, после чего возвращаем полученный результат:

$newurl = $matches[1].$newgp.$anchor;
return $newurl;

Нередко наибольшую трудность в коде вызывают именно регулярные выражения. Если у кого-то с этим проблемы — охотно отвечу в комментариях.

1. Работа с PCRE-совместимыми регулярными выражениями описана на соответствующей странице руководства по PHP.

2. Более строгое регулярное выражение, описывающее URL, можно найти в стандарте, описывающем URI (и, соответственно, URL как его частный случай), однако, в данном случае работать оно будет аналогично приводимому в данной статье: модификатор ? в первой подмаске делает квантификатор + нежадным, поэтому совпадение с первой подмаской продолжается вплоть до первого знака вопроса, после чего задействуется уже вторая подмаска. По той же причине совпадение со второй подмаской прекращается, как только встречается решётка, в противном случае продолжается до конца строки.

3. С переменным количеством подмасок приходится иметь дело довольно-таки часто. В регулярном выражении /^([^?]+)(\?.*?)?(#.*)?$/ гарантировано совпадает только первая подмаска. Вторая же может совсем отсутствовать в случае статического (без GET-запроса) адреса без якоря - именно для описания такого случая и служит код

$gp = (isset($matches[2])) ? $matches[2] : '';

В случае статического адреса с якорем обязана совпасть третья подмаска, поэтому вторая будет обязательно пронумерована, хотя и будет содежать пустую строку.

4. Вообще говоря, если положиться на корректность URL (т.е. допустить в т.ч., что знак вопроса встречается в адресе только один раз и решётка, если присутствует, следует за ним, а не перед ним), то можно использовать более простой код:

function rgp($url) { // remove GET-parameters from URL
    return preg_replace('/\?.*?(?=#|\z)/', '', $url);
    // конструкция вида (?=что_то) - т.н. "заглядывание назад" - проверка последующего текста на совпадение со строкой 'что_то'
    // данная строка совпадет, если за ней следует либо решётка, либо конец данных, обозначаемый служебной последовательностью \z
}

В принципе, уязвимость такого кода достаточно низкая: GET-параметры он успешно уберет, просто может затронуть находящуюся за решёткой-якорем часть строки, если она будет иметь вид ?что_то#что_то_еще?еще_что_то#и_т_д.... В большинстве случаев эта особенность поведения не имеет никакого значения, однако, о ней лучше знать.


© Все права на данную статью принадлежат порталу webew.ru. Перепечатка в интернет-изданиях разрешается только с указанием автора и прямой ссылки на оригинальную статью. Перепечатка в печатных изданиях допускается только с разрешения редакции.
Добавить комментарий
Отображение комментариев: Древовидное | Плоское

bur

В качестве замены функции ggp и для упрощения sgp не удобнее ли пользоваться масивом $_GET ?
Fastcoder.org — портал для JavaScrpt-программистов
02.07.2008, 12:21
Ответить

1234ru

Как мне видится, зачастую упрощения не получится, а будет, напротив, серьезное усложнение.

Представь себе, например, такую модельную задачу: есть форум, у него два (как минимум) параметра - режим просмотра (view) и что-нибудь (например, еще один режим просмотра).
Получается URL вида /showforum.php?topicid=123?view=flat&showads=1#post789

Так вот. И где-нть у тебя есть "карман" с какими-нибудь похожими темами (ну, и т.п.), причем надо, чтобы с этого места человек попадал на такой же режим просмотра темы, который ты задаешь в т.ч. с помощью GET-параметров. При формировании ссылок ты столкнешься с тем, что статическую часть (плюс, возможно, якорь) тебе нужно будет менять, а строку GET оставлять неизменной.
Вряд ли в таком случае будет удобнее руками перебирать ключи массива $_GET и лепить из них строку, предварительно еще и вылавливая из них нужные, чем в одну-две коротких строки сделать то же самое с использованием ggp().

Что там было бы с заменой параметров, я себе представляю уже достаточно смутно, но мне кажется, что еще более полная ж...

Еще не стоит забывать про случаи, при которых строка GET-запроса не соответствует текущему содержимому массива $_GET - в таком случае для обсуждаемой задачи он становится совершенно непригодным (например, обрабатываемый URL относится не к текущей, а к какой-то другой странице т др.).
То, что не убивает нас, делает нас инвалидами.
02.07.2008, 13:34
Ответить
NO USERPIC

aego

Имхо ужос =)
Гораздо проще и интуитивно понятнее реализовать это на parse_url -> explode|implode -> http_build_query
Автор фанат регэкспов?
03.07.2008, 12:42
Ответить

1234ru

Автор действительно очень большой фанат регэкспов :). Поэтому насчет интуитивной понятности кода Вы, наверное, правы (не очень много народу любят регулярные выражения).

С другой стороны, http_build_query() и parse_url() спасают убоявшихся регэкспов только на стадии получения GET-параметров.
Ну хорошо, вместо
preg_match('/^(.+?)(\?.*?)?(#.*)?$/', $url, $matches);
return $matches[2];

напишем

return parse_url($url, PHP_URL_QUERY);

Да, пока, возможно, короче.

Затем, как я понимаю, $vars = explode('&', $query). А дальше как?
foreach ($vars as $str) опять explode() и там проверять по имени ключа?
Не знаю, не знаю, я бы не сказал, что сильно удобнее. Скорее, кто как привык.

Вообще, честно говоря, мне сложно судить о предолженном Вами подходе, т.к. я сам никогда таким образом не писал (так, прикинул быстренько, как всё получится). Выложили бы нам свой код (если не секрет, конечно) - вот мы бы и сравнили ;)

Плюс еще вот что:

- Ни parse_url(), ни http_build_query() не работают с относительными URL (это если вдруг кому понадобится)
- http_build_query() в PHP не ниже 5 (это мало кому будет помехой, но мало ли).
То, что не убивает нас, делает нас инвалидами.
08.07.2008, 16:40
Ответить
NO USERPIC

Sign

Цитата:
Автор действительно очень большой фанат регэкспов
и за это не может не быть уважаем)
Но по статистике независимо от используемого языка количество ошибок в коде это почти константная величина (естественно для каждого она разная), поэтому regexp-ы юзать необходимо, но и именно по этому в данном случае они не нужны, imho

Цитата:
Ни parse_url(), ни http_build_query() не работают с относительными URL

не могу согласиться ибо
[php:]
$url = 'path/qwe/asd.php?arg1=value1&arg2=value2&arr[]=foo+bar&arr[]=baz#anchor';
$parsed_url = parse_url($url);
if($parsed_url['query'])
    parse_str($parsed_url['query'], $parsed_url['query']);
print_r($parsed_url);

[Output:]
Array
(
    [path] => path/qwe/asd.php
    [query] => Array
        (
            [arg1] => value1
            [arg2] => value2
            [first] => value
            [arr] => Array
                (
                    [0] => foo bar
                    [1] => baz
                )

        )

    [fragment] => anchor
)

А с абсолютными путями всё ещё удобней: Вы также получаете протокол, порт, юзера и пароль
14.08.2008, 15:24
Ответить

1234ru

Проглядел ваш комментарий, поэтому отвечаю не совсем своевременно :).

Sign
> Ни parse_url(), ни http_build_query() не работают с относительными URL

не могу согласиться


Да, вижу; действительно работает. Ну, это они написали, а я им поверил:
http://ru2.php.net/parse_url
Цитата:
This function doesn't work with relative URLs.


Вообще, конечно, регулярные выражения мучают компьютер сильно. Поэтому, например, моя функция работает где-то в 25 раз медленнее, чем просто parse_url($url, PHP_URL_QUERY). Признаю своё решение непроизводительным.

А вообще в вашем сообщении есть шокировавшая меня информация: я-то жил и не знал, что в GET-параметрах можно массивы передавать :-O (первый раз увидел это в вашем примере).
Для массивов моя функция работать корректно не сможет, т.к. там надо еще и выяснять, который из элементов хотят заменить, что с точки зрения удобства использования имхо уже граничит с безумием.
В общем, как-то я щас подумал, функция получается, во-первых, медленная, во-вторых, неуниверсальная. Но я уж, наверное, не буду статью убирать - пускай потомки познакомятся с примером использования рег. выражений, пусть и не очень, как оказалось, жизненным.
То, что не убивает нас, делает нас инвалидами.
13.10.2008, 05:21
Ответить

bur

С массивами в get-параметрах приходится сталкиваться, когда через гет отправляется форма с чекбоксами.
К примеру у тебя есть набор чекбоксов с одинаковыми именами и разными значениями:

<input type="checkbox" name="arr" value="1" />
<input type="checkbox" name="arr" value="2" />
<input type="checkbox" name="arr" value="3" />


Чтобы чекбоксы не перезаписывали друг друга к имени добавляются квадратные скобки, которые явно указывают, что значения нужно собрать в массив:

<input type="checkbox" name="arr[]" value="1" />
<input type="checkbox" name="arr[]" value="2" />
<input type="checkbox" name="arr[]" value="3" />


Тогда и имеем урл из примера, который отлично обрабатывается в PHP, по-моему даже в массиве $_GET[] всё адекватно.
Fastcoder.org — портал для JavaScrpt-программистов
13.10.2008, 11:14
Ответить

1234ru

Ясно. Хорошо, что есть такая возможность.
Мне кажется, бывает осмысленно делать даже не name="arr[]", а name="arr[1]", name="arr[2]" и т.п.
То, что не убивает нас, делает нас инвалидами.
13.10.2008, 17:31
Ответить
NO USERPIC

rgbeast

При использовании регэкспов сохраняется порядок следования GET-параметров. Иногда это удобно, так как поисковики считают различный порядок параметров - различными URL.
08.07.2008, 22:45
Ответить
NO USERPIC

AlexNZ

На первый взгляд статья более академического плана, нежели практического, однако приведеные функции удобны в использовании. Еще один важный момент. Regexp нужно знать обязательно, прятать тут голову в песок всё равно что поставить крест на своей работе (конечно, если вы заняты программированием серьёзно).

Использование $_GET не приносит проблем на практике, поскольку в грамотно спроектированном приложении всё должно быть "по полочкам". В таком срипте точно известно что и откуда ожидается, всё остальное нужно отфильтровать. Если у вас возникает путаница, задумайтесь, быть может стоит изменить структуру приложения.

Не стоит таскать в URL режимы просмотров и им подобные понятия, используйте сессии. Используйте для сессий cookie и перестанем уже говорить, что cookie у кого-то там отключены. Вы пишете форум для параноиков?

В чем недостаток regexp? (Это риторический вопрос). Они работают медленно, но за гибкость, которую они предоставляют надо платить, таков наш мир. Используйте другие средства по возможности.

Выражаю автору благодарность за статью, а если вы еще слабы в regexp, обязательно займитесь ими прямо сейчас. :-)
06.08.2008, 09:19
Ответить

1234ru

AlexNZ, спасибо на добром слове. Рад, что Вам понравилось.
То, что не убивает нас, делает нас инвалидами.
06.08.2008, 18:25
Ответить

Ed

Никто не может претендовать на идеальное решение. Но на решение текущей задачи - это позволительно.
Данное решение решает именно таковую.
Спасибо за статью она позволила мне двигатся дальше.

исправил маленький баг в функции sgp:
при ситуации
$url = '/article.php';
echo sgp($url, 'view', ''); // выведет '/article.php&page=3&mode=1#note_1'

выводится /article.php?view=

решение:
preg_match('/^(.+?)(\?.*?)?(#.*)?$/', $url, $matches);
$gp = (isset($matches[2])) ? $matches[2] : ''; // GET-parameters
//проверяем если GET существует
if (!isset($matches[2])) { return $url;}
11.07.2009, 17:21
Ответить

1234ru

Да, действительно. Спасибо за замечание. Внес в статью исправление.
То, что не убивает нас, делает нас инвалидами.
12.07.2009, 05:56
Ответить
NO USERPIC

smart1k

Два дня искал что-то подобное. Огромное спасибо за скрипт!
23.09.2009, 02:47
Ответить
Добавить комментарий
Отображение комментариев: Древовидное | Плоское
© 2007—2010 webew.ru, связаться: x собака webew.ru
Сайт использует Flede и соответствует стандартам WAI-WCAG 1.0 на уровне A.
Rambler's Top100