Человекопонятный URL (ЧПУ) - одна из самых часто затрагиваемых тем на различных форумах, посвящённых языку PHP. Можно до бесконечности спорить, нужны ли красивые URL-адреса для SEO-оптимизации, но факт того, что веб-сайт с ЧПУ выглядит аккуратно и профессионально отрицать глупо.
URL адрес вида
http://server.com/catalog/tv/samsung/5
выглядит опрятно и интуитивно понятно для пользователя, нежели адрес вида
http://server.com/catalog.php?product=tv&brand=samsung&page=5
Кроме того, более-менее продвинутый пользователь в строке адреса броузера может стереть или заменить определенный путь иерархии в URL адресе, попав тем самым в нужное для него место на сайте (об этом хорошо написал Лебедев ещё в 2000 году - "Боремся за чистоту урлов", "Дублирующая навигация").
ЧПУ на базе модуля Apache mod_rewrite
Долгое вря ЧПУ делались с помощью небезызвестного модуля веб-сервера Apache mod_rewrite, который предназначен для манипуляции URL адресами. Директивы для mod_rewrite обычно писались разработчиками в .htaccess файле конфигурации Apache и выглядели примерно так:
RewriteEngine on
RewriteBase /
# Досье пользователя.
RewriteRule ^users/([0-9]+)\.html$ userinfo.php?user_id=$1
# Список всех пользователей
RewriteRule ^users/?$ users.php?%{QUERY_STRING}
# Новости общим списком
RewriteRule ^news/?$ news.php
# Новости по разделам
RewriteRule ^news/([a-z_]+)/?$ news.php?cat=$1
# Страница одной новости
RewriteRule ^news/([0-9]+).html$ news.php?id=$1
Чем плох подобный механизм преобразований URL адресов, освоенный на mode_rewrite? Ничем не плох, но для разработки гибких веб-приложений он не подходит. В первую очередь потому, что преобразованием занимается сам mod_rewrite и система фактически завязана на файле конфигурации .htaccess. Мы не имеем возможности влиять на процесс преобразования, добавлять в автоматическом режиме новые виртуальные URL адреса, привязывать сложную логику к определённым виртуальным URL-адресам и делать многое другое.
ЧПУ на PHP
Конечно же, на одном PHP ЧПУ не сделать. Моудуль mode_rewrite как и раньше нужно использовать, но только лишь для того, что бы перенаправить все запросы к виртуальным ЧПУ-адресам в единую точку входа в приложении - в index.php. Для этого в .htaccess файле конфигурации можно прописать следующий код:
RewriteEngine on
RewriteBase /
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ [NC,L]
RewriteRule ^.*$ index.php [NC,L]
Данная запись означает буквально следующее: если запрошенный URL-адрес не является файлом, не является символической ссылкой и не является директорией, то подменить виртуальный адрес файлом index.php. При этом, суперглобальная переменная PHP $_SERVER['REQUEST_URI'] будет содержать именно запрошенный виртуальный адрес. Что это нам даёт? Фактически - безграничные возможности для манипулирования виртуальными адресами!
Пример № 1.
Во многих фреймворках ЧПУ строится по следующему принципу:
http://server.com/модуль/действие/параметр1/значение1/параметр2/значение2/параметрН/значениеН/
"Распарсить" такой URL и получить имя модуля, действие и параметры нет никаких проблем.
В данном случае под определением "модуль" и "действие" можно понимать что угодно:
Модуль - подключаемый файл, действие - функция или конструкция в блоке if-else
Модуль - класс, действие - метод класса
т.е. все зависит от вашей архитектуры приложения.
Далее по тексту "модуль" и "действие" будет также называться совокупным определением "обработчик".
Возьмем в пример такой URL-адрес:
http://localhost/guestbook/edit_message/id/123
и "распарсим" его с помощью нижестоящего кода, который поместим в единую точку входа - в index.php:
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Назначаем модуль и действие по умолчанию.
$module = 'index';
$action = 'index';
// Массив параметров из URI запроса.
$params = array();
// Если запрошен любой URI, отличный от корня сайта.
if ($_SERVER['REQUEST_URI'] != '/') {
try {
// Для того, что бы через виртуальные адреса можно было также передавать параметры
// через QUERY_STRING (т.е. через "знак вопроса" - ?param=value),
// необходимо получить компонент пути - path без QUERY_STRING.
// Данные, переданные через QUERY_STRING, также как и раньше будут содержаться в
// суперглобальных массивах $_GET и $_REQUEST.
$url_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Разбиваем виртуальный URL по символу "/"
$uri_parts = explode('/', trim($url_path, ' /'));
// Если количество частей не кратно 2, значит, в URL присутствует ошибка и такой URL
// обрабатывать не нужно - кидаем исключение, что бы назначить в блоке catch модуль и действие,
// отвечающие за показ 404 страницы.
if (count($uri_parts) % 2) {
throw new Exception();
}
$module = array_shift($uri_parts); // Получили имя модуля
$action = array_shift($uri_parts); // Получили имя действия
// Получили в $params параметры запроса
for ($i=0; $i < count($uri_parts); $i++) {
$params[$uri_parts[$i]] = $uri_parts[++$i];
}
} catch (Exception $e) {
$module = '404';
$action = 'main';
}
}
echo "\$module: $module\n";
echo "\$action: $action\n";
echo "\$params:\n";
print_r($params);
Вот так будет выглядеть результат для URL-адреса http://localhost/guestbook/edit_message/id/123:
$module: guestbook
$action: edit_message
$params:
Array
(
[id] => 123
)
Теперь, если у вас есть класс Guestbook, отвечающий за работу гостевой книги, то вы просто инстанцируете этот класс и вызываете его метод отвечающий за редактирование сообщения. Массив $params можно передать в метод в качестве аргумента метода:
view sourceprint?
1.
$guestbook = new $module();
2.
$guestbook->$action($params); // редактирование сообщения с ID = 123
но лучше всего при подобном подходе поместить все полученные переменные из парсинга URL адреса в суперглобальный массив $_REQUEST:
$_REQUEST = array_merge($_REQUEST, $params);
Если же будет запрошен ошибочный URL -адрес:
http://localhost/guestbook/edit_message/id/
то сработает исключение и в качестве обработчика будет фигурировать обработчик 404 ошибки:
$module: 404
$action: main
$params:
Array
(
)
Пример № 2.
Для понимания следующего подхода вам необходимы базовые знания Perl-совместимых регулярных выражений (POSIX).
Данный подход очень гибок и интересен, т.к. позволяет создавать практические любые виртуальные адреса, с абсолютно произвольной структурой и в отличие от предыдущего подхода - без упоминания в URL реального имени обработчика (модуля или действия). Суть метода заключается в следующем: каждый виртуальный URL-адрес, который может быть в системе, необходимо описать в виде регулярного выражения. Назначить для него обработчик. Если запрошенный URL совпадает с одним из таких регулярных выражений, то с помощью функций регулярных выражений нужно получить из URL необходимые данные, после чего передать их в обработчик. Пример:
// Конфигурация маршрутов URL проекта.
$routes = array
(
// Главная страница сайта (http://localhost/)
array(
// паттерн в формате Perl-совместимого реулярного выражения
'pattern' => '~^/$~',
// Имя класса обработчика
'class' => 'Index',
// Имя метода класса обработчика
'method' => 'index'
),
// Страница регистрации пользователя (http://localhost/registration.xhtml)
array(
'pattern' => '~^/registration\.xhtml$~',
'class' => 'User',
'method' => 'registration',
),
// Досье пользователя (http://localhost/userinfo/12345.xhtml)
array(
'pattern' => '~^/userinfo/([0-9]+)\.xhtml$~',
'class' => 'User',
'method' => 'infoInfo',
// В aliases перечисляются имена переменных, которые должны быть в дальнейшем созданы
// и заполнены значениями, взятыми на основании разбора URL адреса.
// В данном случае в переменную user_id должен будет записаться числовой
// идентификатор пользователя - 12345
'aliases' => array('user_id'),
),
// Форум (http://localhost/forum/web-development/php/12345.xhtml)
array(
'pattern' => '~^/forum(/[a-z0-9_/\-]+/)([0-9]+)\.xhtml$~',
'class' => 'Forum',
'method' => 'viewTopick',
// Будут созданы переменные:
// forum_url = '/web-development/php/'
// topic_id = 12345
'aliases' => array('forum_url', 'topic_id'),
),
// и т.д.
);
Как видно из примера, так нужно будет описать все виртуальные URL адреса проекта. Напишем обработчик таких URL адресов:
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Тут нужно подключить через include файл с маршрутами, который описали выше.
// Назначаем модуль и действие по умолчанию.
$module = 'Not_Found';
$action = 'main';
// Массив параметров из URI запроса.
$params = array();
foreach ($routes as $map)
{
// Для того, что бы через виртуальные адреса можно было также передавать параметры
// через QUERY_STRING (т.е. через "знак вопроса" - ?param=value),
// необходимо получить компонент пути - path без QUERY_STRING, т.к. в ином
// случае виртуальный адрес попросту не совпадет ни с одним паттерном из массива $routes.
// Данные, переданные через QUERY_STRING, также как и раньше будут содержаться в
// суперглобальных массивах $_GET и $_REQUEST.
$url_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if (preg_match($map['pattern'], $url_path, $matches))
{
// Выталкиваем первый элемент - он содержит всю строку URI запроса
// и в массиве $params он не нужен.
array_shift($matches);
// Формируем массив $params с теми названиями ключей переменных,
// которые мы указали в $routes
foreach ($matches as $index => $value)
{
$params[$map['aliases'][$index]] = $value;
}
$module = $map['class'];
$action = $map['method'];
break;
}
}
echo "\$module: $module\n";
echo "\$action: $action\n";
echo "\$params:\n";
print_r($params);
Теперь при запросе
http://localhost/forum/web-development/php/12345.xhtml
мы получим информацию о нужном нам обработчике (классе и методе класса), а так же массив параметров, с помощью которых и получим из СУБД информацию о теме в форуме:
$module: Forum
$action: viewTopick
$params:
Array
(
[forum_url] => /web-development/php/
[topic_id] => 12345
)
Соответственно, инстанцирование нужного обработчика, т.е. класса Forum и запуск его метода viewTopick аналогичны запуску обработчика из примера №1.