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

Централизация обработки запросов к сайту с помощью mod_rewrite

26 мая 2009, 12:36

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

Для чего это нужно.

На многих сайтах встречаются страницы вида /news.php?id=123, /products.php?id=45, /about.php, /contact.php и т.п. Как правило, каждой такой странице соответствует отдельный скрипт с точно совпадающими названием и расположением, что уже неприятно, т.к. накладывает определенные ограничения и, таким образом, увеличивает вероятность ошибки при разработке сайта. Что еще (и гораздо) более неприятно: большинство страниц сайта имеет повторяющиеся фрагменты HTML, которые генерируются одним и тем же кодом в скриптах; соответственно, этот код должен быть повторен столько раз, сколько есть скриптов.

Что может дать описываемый подход? Для начала он позволит избавиться от нескольких файлов, не отказываясь при этом от вида адресов /scriptname.php?get=..., и заменить их одним рабочим скриптом (или не одним; в любом случае, столькими, сколькими захочется, а не сколькими потребует формат адреса).

На самом деле такой подход может дать гораздо больше. При таком подходе относительный адрес страницы перестает зависеть от физического расположения файлов и каталогов и становится некой абстрактной строкой, вид которой ограничивается только стандартом URL. Это значит, что при формировании ответа клиенту можно сделать вид, что есть, например, каталог /news/ и страница /news/123.html (или что-то еще более сложное), хотя ни каталога, ни файла с такими названиями в физической файловой системе нет и в помине.

Замена адресов вида /news.php?id=123 и /articles.php?id=45 на адреса вида /news/123.html и /articles/45.html отнюдь не лишена смысла: такой формат адресов обладает лучшей читаемостью, кроме того, есть информация, что для поисковых систем также легче проиндексировать две фактически разные страницы, если они имеют разные адреса, чем в случае, когда адреса отличаются только строкой GET-запроса.

Что такое mod_rewrite.

mod_rewrite — один из модулей веб-сервера Apache. mod_rewrite предоставляет мощные и гибкие средства для разнообразных манипуляций с URL. Для решения настоящей задачи, однако, понадобится лишь малая часть функциональности этого модуля, которая и будет рассмотрена ниже. mod_rewrite посвящен раздел официального сайта Apache.

.htaccess

Одним из самых гибких методов настройки сервера Apache является использование конфигурационных файлов .htaccess. Установки из этих файлов имеют более высокий приоритет, чем установки в конфигурационных файлах httpd.conf сервера Apache (если не запрещена замена установок), и действуют на каталог, в котором находится данный файл .htaccess, а также на все дочерние каталоги (если только в каких-либо из них нет своих файлов .htaccess — тогда установки из .htaccess текущего каталога аннулируют установки из .htaccess родительского).

Сейчас наша основная задача состоит в том, чтобы централизовать управление сервером — передать его какому-то одному скрипту, пусть это будет, например, скрипт index.php в корневом каталоге веб-сервера. Поскольку мы хотим перенаправлять таким образом абсолютно любой запрос, файл .htaccess следует создать также в корневом каталоге. Для решения нашей задачи этот файл должен содержать всего две строчки:

RewriteEngine on
RewriteRule .*? index.php

Директива RewriteEngine on отвечает за включение механизма mod_rewrite (без неё он просто не начнет работать). Директива RewiteRule не так проста в использовании и также является исключительно важной в описываемой реализации, поэтому обсудим её подробнее.

Как уже говорилось, основная часть задачи состоит в том, что необходимо передать управление. Этим как раз и занимается директива RewriteRule. Синтаксис использования этой директивы в .htaccess таков:

RewriteRule условие что_делать
(подробнее см. http://httpd.apache.org/docs/2.0/mod/mod_rewrite.html#rewriterule).

В качестве условия выступает Perl-совместимое регулярное выражение (mod_rewrite использует библиотеку PCRE), которое сравнивается со строкой URL (если запрошен адрес http://site.ru/news/123.html, то строкой URL будет /news/123.html). Поскольку нам требуется запустить механизм в любом случае, следует составить такое регулярное выражение, которое совпало бы с любой строкой. Для этого хорошо подходит .*?.

Второй аргумент директивы (что_делать) — это указание серверу о том, как ему изменить адрес запроса. Нам нужно абсолютно все запросы направить к скрипту index.php, находящемся в том же каталоге, что и .htaccess; это и отражено в директиве.

Что теперь должен делать index.php

Вот как может выглядеть вышеупомянутый скрипт index.php:

<?php
/*
Здесь и должен формироваться код для всех возможных URL (т.е. для всех страниц).
Если раньше всё это было рассредоточено по разным файлам, то теперь всё - здесь.
По непосредственно рабочему коду отличия минимальные: как формировали HTML специфические
предназначенные для этого функции, так и будут формировать.
Главное - не забыть, что одинаковые части страницы
типа шапки формируются один раз, и не наделать повторяющегося кода.

Следует отметить, что несмотря на манипуляции с адресом запроса,
cодержимое переменной $_SERVER['REQUEST_URI'] останется прежним,
и можно будет работать с тем адресом запроса, который был в самом начале.
*/

$url = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); // GET-запрос пока уберём

// разберем различные случаи вида url.
if ($url == '/') {
    // здесь выводим HTML для главной страницы; её адрес - /
    // хотя более удобным было бы для всякой страницы HTML-код
    // всё же формировать динамически
}
elseif (preg_match('/^\/news\/(\d+)\.html$/', $url, $matches) {
    // обрабатываем страницы вида /news/123.html
    $newsid = $matches[1];
    // далее получаем текст новости по id, внедряем текст в код страницы и т.п.
}
elseif (preg_match('/^\/articles\/(.+)\.html$/', $url, $matches))
    // обрабатываем страницы вида /articles/some_article.html
    $article_name = $matches[1];
    // получаем данные статьи по её имени
}
else {
    /*
    Если ничего из вышеперечисленного не сработало, значит, запрошенный адрес
    не относится ни к статье, ни к новости, а относится к чему-либо еще
    (например, к одной из уникальных страниц, вид которых
    строится индивидуально для каждой из них).
    Возможно, страницы с запрашиваемым адресом на сайте не существует,
    тогда клиенту нужно вернуть HTTP-код ошибки - 404 - и информацию о том,
    что страница не найдена.
    */

}
   
    /*
    Теперь можно каким-либо способом (например, при помощи какого-нибудь шаблонизатора)
    построить однотипные для всех страниц сайта фрагменты HTML-кода
    (например, объявление DOCTYPE, заголовок и шапку страницы),
    после чего вывести полученный код в окно браузера.
   
    Разумеется, всё это может принимать сколь угодно разнообразные формы.
    */

   
?>

Если все-таки кое-где нужны физические файлы и каталоги.

Бывает, однако, так, что в ряде случаев по тем или иными причинам использовать физические файлы и каталоги удобнее, чем формировать динамический ответ — например, в случае картинок или других двоичных файлов (хотя существует альтернативный способ хранения и обработки файлов, основанный на использовании СУБД). В рамках данного подхода это может быть реализовано двумя способами.

Собственные файлы .htaccess в подкаталогах

Как уже упоминалось, установки в .htaccess текущего каталога имеют приоритет над установками родительских всех уровней. Причем эти установки действуют не только на сам текущий каталог, но и на его дочерние каталоги. Этим можно воспользоваться в каждом случае, когда имеется каталог (пусть /pics/), содержимое которого должно быть доступно напрямую. Для этого нужно в нём создать файл .htaccess, содержащий всего одну строчку:

RewriteEngine off

Такой файл просто снимет все имеющиеся предписания насчет перенаправления запросов для каталога /pics/ и его дочерних каталогов, и изнутри /pics/ всё будет выглядеть так, как будто mod_rewrite вообще не включен.

Следует отметить, что понятие «изнутри каталога» относится также и ко вложенным каталогам — доступ к ним тоже будет прямым, для этого создавать .htaccess в каждом из них не нужно.

Условия !-d и !-f в корневом .htaccess

Этот вариант реализуется на уровне корневого каталога и состоит в ограничении правила RewriteRule определенными условиями.

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

В примерах выше мы не использовали условий, поскольку правила должны были действовать в любом случае. Теперь же необходимо составить условия — такие, которые запретили бы правилу действовать в случаях, когда запрашиваемый адрес является путем к физическому файлу, или, немного перефразируя, условия дейсвтия должны быть таковы, чтобы запрошенный адрес не являлся путем к файлу. С учетом всего этого .htaccess будет выглядеть так:

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .*? index.php
# в переменной сервера REQUEST_FILENAME как раз и хранится адрес запроса

Такая конфигурация обеспечит доступ не только к файлам в текущем каталоге, но и в дочерних (т.е. для того, чтобы стали видны файлы /pics/123.jpg, специально делать больше ничего не понадобится). В то же время, адреса, соответствующие физическим каталогам, по-прежнему будут переписываться. Это приведет к тому, что списки содержимого каталогов просматривать будет нельзя (запросив /pics/, вместо списка картинок клиент получит перенаправление на /index.php). Если все-таки нужна видимость и каталогов тоже, то .htaccess следует дополнить соответствующим условием для каталогов, и он примет такой вид:

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .*? index.php

Подробнее о составлении условий можно узнать из соответствующей страницы документации к серверу Apache.

Сравнение двух способов.

Первый способ является более безопасным, поскольку общие установки более строгие. Чтобы их отменить, нужно совершить специальные операции — это и повышает безопасность, поскольку программист яснее осознает, какие области он делает доступными. Это же и снижает удобство: если безопасность не нужна, то легче написать две строчки в корневом .htaccess, чем проводить отдельную работу для каждой директории.

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


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

M-A-X

>Замена адресов вида /news.php?id=123 и /articles.php?id=45 на адреса вида /news/123.html и /articles/45.html отнюдь не лишена смысла: такой формат адресов обладает лучшей читаемостью

Та читаемость одинакома. А если вручную менять номера для перехода, то удобнее, чтобы было вида /news.php?id=123, курсор проще в конец строки установить :)

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

Уже вроде нет :)

> Собственные файлы .htaccess в подкаталогах

Как уже упоминалось, установки в .htaccess текущего каталога имеют приоритет над установками родительских всех уровней. Причем эти установки действуют не только на сам текущий каталог, но и на его дочерние каталоги. Этим можно воспользоваться в каждом случае, когда имеется каталог (пусть /pics/), содержимое которого должно быть доступно напрямую. Для этого нужно в нём создать файл .htaccess, содержащий всего одну строчку:
RewriteEngine on


Может лучше RewriteEngine off? :)

И есть информация, что mod_rewrite немного нагружает сервер.
http://kpitv.net/
11.08.2009, 01:34
Ответить

1234ru

Цитата:
Та читаемость одинакома. А если вручную менять номера для перехода, то удобнее, чтобы было вида /news.php?id=123, курсор проще в конец строки установить :)

А если /news.php?id=123&view=flat&start=150 ? :)

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

Цитата:
Может лучше RewriteEngine off? :)

И правда :) Поправил статью

Цитата:
>кроме того, есть информация, что для поисковых систем также легче проиндексировать две фактически разные страницы, если они имеют разные адреса, чем в случае, когда адреса отличаются только строкой GET-запроса.

Уже вроде нет :)

И давно это? (ссылку какую-нть бы, что ли...)

Цитата:
И есть информация, что mod_rewrite немного нагружает сервер.

Бывает ли это критично?

Кстати, а как Вы проектируете свои приложения, какими технологиями пользуетесь?
То, что не убивает нас, делает нас инвалидами.
14.08.2009, 22:56
Ответить
NO USERPIC

M-A-X

1234ru
А если /news.php?id=123&view=flat&start=150 ? :)

Ну это уже на форум похоже :)

1234ru
И давно это? (ссылку какую-нть бы, что ли...)


http://promosite.ru/articles/report-optimization-2002.php
Абзац "Геометрия сайта и ее оптимизация"
Ссылка за 2002 год. Думаю, уже точно нет некоторых оставшихся на то время проблем.
Правда, в Яндексе часто при запросе первыми выводятся сайты с входжением ключевого слова в юрл.
Других ссылок под рукой нет. Можно Ваши ссылки? :)

1234ru
Бывает ли это критично?

Встречал такую информацию.

Проверил сам только что.
ab -n 150 -c 10 -g и
ab -n 1500 -c 100 -g
С mod_rewrite и одним условием время отклика не увеличилось.

1234ru
Кстати, а как Вы проектируете свои приложения, какими технологиями пользуетесь?]


Сначала это было хобби.
Как проектирую? Это сложно рассказать. :) Стараюсь, чтобы приложение создавало минимальную нагрузку и было расширяемым. Супер-профи себя не считаю :)
http://kpitv.net/
19.08.2009, 18:50
Ответить

1234ru

Цитата:
Других ссылок под рукой нет. Можно Ваши ссылки? :)

Признаюсь, что ссылками особо не располагаю :о (я сам в этом очень слабо разбираюсь - рассказывали друзья-SEOшники)

Цитата:
Как проектирую? Это сложно рассказать. :) Стараюсь, чтобы приложение создавало минимальную нагрузку и было расширяемым.

Ну, это прекрасно :)

То, что не убивает нас, делает нас инвалидами.
19.08.2009, 23:55
Ответить
NO USERPIC

M-A-X

Все таки да, удобней с одной точкой входа :)
http://kpitv.net/
08.09.2011, 13:59
Ответить

Zlomovsky

И что, у вас в PHP-файле работает такая конструкция "case preg_match" при switch $url?

preg_match возвращает количество вхождений шаблона. Получается, что содержимое строки $url сравнивается с количеством вхождений нужной подстроки. Это всё равно, что сравнивать вкус помидоров и вес гвоздей с точки зрения особенностей цветовосприятия сусликов в брачный период.

По-моему, подходить по смыслу (и уж точно корректно работать) будет что-нибудь вроде:

$url = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); //Как и было раньше

preg_match('/^\/([a-zA-Z0-9_\.\-]*)(.*)$/', $url, $matches);

switch ($matches[1]) {
  case 'news':
    //Если адрес был /news/123.html,
    //то в $matches[2] в данный момент
    //хранится значение /123.html
    //Регулярными выражениями вытаскиваем
    //необходимые параметры
    break;
  case 'article':
    //Если адрес был /articles/some_article.html,
    //то в $matches[2] в данный момент
    //хранится значение /some_article.html
    //Регулярными выражениями вытаскиваем
    //необходимые параметры
    break;
  default:
    //Пустое или неучтённое значение
    //переменной $matches[1]
}
http://skoli.ru
03.05.2012, 06:42
Ответить
NO USERPIC

rgbeast

В PHP есть второй синтаксис для case, при котором в последнем передается не число, а условие. Например, такой код будет работать корректно:

$x=5;
switch($x) {
case (preg_match('/[0-4]/',$x)>0): echo "match 0-4\n"; break;
case (preg_match('/[5-9]/',$x)>0): echo "match 5-9\n"; break;
}


Без условия >0 в моей версии PHP это не работает, но можно обойти, пользуясь тем, что preg_match возвращает 0 или 1.

$x=5;
switch(1) {
case preg_match('/[0-4]/',$x): echo "match 0-4\n"; break;
case preg_match('/[5-9]/',$x): echo "match 5-9\n"; break;
}

03.05.2012, 09:18
Ответить

Zlomovsky

Но в ваших примерах-то ведь сравнивается число в switch и число в case, которое возвращает preg_match.

А в статье сравнивается строка $url и число, которое возвращает preg_match.

Пример из статьи не работает так, как это задумано. Если провести что-то вроде отладки, взяв за значение $url строку "/news/123.html", то получится примерно следующее (я заменю все переменные на их значения):

switch ("/news/123.html") //$url
    {
    case 1: //preg_match('/^\/news\/(\d+)\.html$/', $url, $matches)
        //обрабатываем страницы вида /news/123.html
    break;
   
    case 0: //preg_match('/^\/articles\/(.+)\.html$/', $url, $matches)
        //обрабатываем страницы вида /articles/some_article.html
    break;
   
    case 0: //preg_match('/^\/$/', $url, $matches)
        //здесь выводим HTML для главной страницы; её адрес - /
    break;
   
    /* и аналогично - другие случаи вида URL, которые стоит обработать отдельно */
   
    default:
      //В итоге, при любом значении $url всё будет перенаправляться сюда - в блок default.
      //Потому что "/news/123.html" не равно 1 и "/news/123.html" не равно 0.
    break;


Помидоры - это помидоры, а гвозди - это гвозди. Тем более, что пример из статьи я опробовал на практике. Он любые запросы сбрасывает в блок default.
http://skoli.ru
03.05.2012, 09:42
Ответить
NO USERPIC

rgbeast

Я хотел сказать про существование второго синтаксиса switch, как в моем первом примере - в этом случае значение из switch вообще ни с чем не сравнивается (по сути игнорируется). Что касается кода в статье, дождемся ответа автора.
03.05.2012, 09:48
Ответить

Zlomovsky

*deleted*

В общем, я не спорю. Но про синтаксис - это не совсем то.
http://skoli.ru
03.05.2012, 09:56
Ответить

1234ru

Честно говоря, конкретно этот пример не был протестирован.
Был же протестирован другой пример, где в case не просто preg_match(), а preg_match AND/OR что-то - это уже возвращает TRUE/FALSE, которая со строкой сравнивается в итоге так, как надо (если строка не пустая, срабатывает case, где условие - TRUE).

Вообще говоря, так писать не очень правильно - плохо отражает логику алгоритма. Поэтому в примере в тексте статьи я заменил switch на набор if/else.

Zlomovsky
preg_match('/^\/([a-zA-Z0-9_\.\-]*)(.*)$/', $url, $matches);

switch ($matches[1]) ...


Так не пойдет..

Во-первых, в случае отсутствия совпадения с первой подмаской (например, для адреса главной страницы) элемента № 1 в массиве $matches не будет и станет генерироваться Warning.

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

Короче, по-моему удобней через if. Но это дело вкуса.
То, что не убивает нас, делает нас инвалидами.
03.05.2012, 19:49
Ответить

Zlomovsky

Я не спорю. Просто пример из статьи не работал. Я привёл один из работающих вариантов - простенькое сито.

Про сусликов я говорил только потому, что пример вообще не работал. А способы реализации... тут суслики ни при чём. Каждый видит логику своими глазами.
http://skoli.ru
04.05.2012, 06:46
Ответить

1234ru

Похоже, близкая к оптимальной конфигурация в общем случае выглядит так:
RewriteEngine on

RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]
RewriteRule .* http://%1/$0 [R=301,L]

RewriteCond %{REQUEST_FILENAME} !-f [OR]
RewriteCond %{REQUEST_FILENAME} \.php$
RewriteCond %{REQUEST_FILENAME} !ajax
RewriteRule .* _analyze-url.php

Первая группа правил включает 301 (moved permanently) с адресов вида "www..." на аналогичные без www.

Вторая группа заведует динамической обработкой адресов. Следует отметить, что в ней специально описан случай, когда запрошенный делается к физическому php-файлу. Так сделано, чтобы защитить скрипты от несанкционированного выполнения - как правило, файлы php на сервере подключаются через include/require_once и не предназначены для выполнения в одиночку.
Исключение составляют php-скрипты, к которым делаются AJAX-запросы. Для них удобным было бы принять соглашение, по которому путь (имя каталога или самого файла) содержит строку 'ajax'. В соответствии с этим соглашением и написано последнее условие в группе.
То, что не убивает нас, делает нас инвалидами.
07.05.2012, 17:24
Ответить
NO USERPIC

rubenduden

preg_match('/^\/([a-zA-Z0-9_\.\-]*)(.*)$/', $url, $matches);

switch ($matches[1]) ...

Не знаю вы как но я делаю следующим образом

запросов вида разделяю с функцией explode одна из моих любимых функций )))

$url = www.mysite.ru/news/123.html;

$path = explode('/',$url);

загнетса в массив $path

$path[0] = www.mysite.ru
$path[1] = news
$path[2] = 123.html

и дальше

switch($path[1])
{
case "news":

загружаю шаблон для news с параметрами path[2] path[3] итд<
break;
case "games"
загружаю шаблон для games с параметрами path[2] path[3] итд<
break;

....итд
}

30.06.2012, 14:24
Ответить

1234ru

Цитата:
$path = explode('/',$url);


Ну да, примерно так.

Только лучше не весь $url, а parse_url($url, PHP_URL_PATH).
То, что не убивает нас, делает нас инвалидами.
01.07.2012, 04:21
Ответить
Добавить комментарий
Отображение комментариев: Древовидное | Плоское
© 2008—2024 webew.ru, связаться: x собака webew.ru
Сайт использует Flede и соответствует стандартам WAI-WCAG 1.0 на уровне A.
Rambler's Top100

Реклама: Проведем обслуживание Postgresql единоразово или на постоянной основе.