Сайт вебмастера

PHP-роутинг (Routing) для новичков

02-02-2016Reading time ~ 6 min.PHP 146724

Роутинг — это маршрутизация: входящий URL разбирается специальным образом и по его результату выполняется определенный код. С роутингом напрямую связано понятие ЧПУ (человекопонятные урлы), которое позволяет исключить в адресах сложные параметры. Например вместо http://сайт/admin/new-page пришлось бы использовать http://сайт/admin.php?action=new-page

Любой входящий URL на сервере разбирается по единому стандарту. Полностью приводить документацию не буду (см. как пример функцию parse_url), важно лишь понять, что в адресе передается параметр path (путь на сервере), которого на сервере реально может не быть. Например в адресе http://сайт/admin каталога admin реально может не существовать.

То есть сервер, получив такой адрес, попытается найти каталог admin, но не найдя его, выдаст 404-страницу (not found).

Чтобы исключить такой вариант, серверу указывается, что для всех несуществующих каталогов и файлов, подключать php-файл (обычно index.php).

Делается это в файле .htaccess с помощью Apache-модуля mod_rewrite. Вот довольно типовой вариант (MaxSite CMS):

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php/$1 [L,QSA]

Тут главная строчка с RewriteRule — именно она определяет шаблон входящего адреса (в примере это регулярное выражение) и что с ним делать. В данном примере будет подключен index.php с параметрами после слэша.

Строчка RewriteCond %{REQUEST_FILENAME} !-f / -d указывает исключить из обработки реально существующие на сервере файлы и каталоги.

Похожий вариант, только чуть короче, от WordPress:

RewriteRule . /index.php [L]

Вариант LPF:

RewriteRule ^(.*)$ /index.php?page=$1 [QSA]

Здесь принудительно добавляется query-параметр page.

Еще один распространенный вариант (пожалуй самый «типовой»):

RewriteRule (.*) index.php?$1 [QSA,L]

Все эти RewriteRule-правила делают простую вещь: как бы «преобразуют» входящий адрес в набор query-параметров. Например адрес http://сайт/admin превратится в http://сайт/index.php?admin

Посмотрите на полный код .htaccess:

<IfModule mod_rewrite.c>
RewriteEngine on
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule (.*) /index.php?$1 [QSA,L]
</IfModule>

Если это какой-то подкаталог, то он указываетс в RewriteBase и как путь к php-файлу. Например каталог на сервере route:

<IfModule mod_rewrite.c>
RewriteEngine on
RewriteBase /route/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule (.*) /route/index.php?$1 [QSA,L]
</IfModule>

Теперь, все адреса на сервере будут передаваться в файл index.php, а исходный адрес сохранится в виде query-параметра.

Если в index.php разместить

<?php
print_r($_GET);

То мы можем увидеть query-параметры. В PHP за это отвечается суперглобальная переменная $_GET. Например для http://сайт/admin это будет admin, для http://сайт/admin/new-pageadmin/new-page.

Таким образом, с помощью .htaccess происходит первая часть роутинга, где мы получаем готовый $_GET.

Кстати, насчет .htaccess WordPress. Он не создает $_GET, поэтому придется использовать $_SERVER['REQUEST_URI'] в который включается подкаталог. Работать с таким адресом уже будет сложней.

Второй этап роутинга выполняется полностью на PHP. Получив $_GET нужно решить что с ним делать. Например если адрес admin, подключить файл admin.php.

Существуют несколько принципиально разных подходов в организации роутинга. Наиболее популярный подход — это когда в адресе передаётся «действие», которое описывается через php-класс. Такой подход хорошо описан в CodeIgniter:

example.com/class/function/id/

Например пусть будет класс admin в нём метод edit, принимающий параметр $id.

example.com/admin/edit/23

class admin {
	function edit($id)
	{
		...
	}
}

Это сильно утрированный пример, но он хорошо показывает соответствие адреса и php-класса.

Другой вариант похожий, но используется не классы, а функции.

example.com/admin/edit/23

function admin($params)
{
	...
}

То есть функция — это первый сегмент, а остальные выступают уже как парметры. Встречается более «продвинутый» вариант.

example.com/admin

function admin($params)
{
	...
}

example.com/admin/edit/23

function admin_edit($params)
{
	...
}

То есть имя функции строится по сегментам URL.

Третий, тоже распространенный вариант — адрес указывает на подключаемый файл.

example.com/admin

$fn = 'pages/admin.php';

if (file_exists($fn))
	reqiure($fn);
else
	reqiure('pages/404.php');

Здесь все файлы хранятся в каталоге pages и подключаются только если реально существуют. Если файла нет, то подключается предопределенный 404-файл.

Если сайт представляет собой сложный проект, то как правило используют роутинг на основе php-классов. Так его проще поддерживать и развивать. Если же сайт состоит из небольшого количества страниц, то как правило используют подключаемые файлы, где один файл — это одна страница.

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

В задачу роутинга входит не только необходимое «действие», но и валидация входящего адреса и его лексический разбор.

Адреса могут строиться по шаблону. Например какой-то адрес должен содержать только номер, а не текст (например example.com/admin/edit/23, но не example.com/admin/edit/hello). Или адрес может быть неизменным, но обработчик будет меняться от вида запроса GET или POST. В одном случае нужно подключить одну функцию/файл, в другом — другой. Бывают и более сложные задачи, все их перечислять нет смысла, поэтому появились разного рода php-библиотеки для организации роутинга.

Свой «велосипед» не изобретал только ленивый, но я отмечу довольно известный FastRoute, который вобрал в себя наиболее типичные решения.

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

$r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1');

Примерно такой же подход используется и в роутинге CodeIgniter.

products/shirts/123
$route['products/([a-z]+)/(d+)'] = "$1/id_$2";

То есть входящий адрес должен соответствовать шаблону и только в этом случае он «сработает».

В FastRoute реализована поддержка POST и GET-запросов. Такая возможность интересна, хотя на больших проектах такие вещи лучше делать на уровне самого «действия». Но это уже тонкости. Про эту библиотеку я упоминаю в первую очередь из-за того, что она достаточно популярна и уже используется в нескольких интересных проектах: Slim и Lumen.

Для небольших проектов, конечно же, FastRoute будет избыточна, поэтому можно ограничиться вариантом попроще, да и мозги потренировать. :-)

ТЗ. Пусть роутинг будет обрабатывать адреса вида http://сайт/blog/contact/map, по которому будет подключен файл content/blog/contact/map/index.php, где каталог content — это общее хранилище всех страниц сайта, а index.php — обязательный файл с кодом страницы. Роутинг должен проверять реальный файл и если его нет, то подключать 404-страницу. Если никаких параметров нет, то это home-страница. Пусть они будут предопределены.

Решение. По сути задача сводится к преобразованию входящего адреса в путь на сервере. У нас есть массив $_GET, где первый ключ и есть входящий URL. Дальше формируем путь к файлу и если он есть, то подключаем. Если нет, то подключаем 404-страницу.

Весь код в 2 строчки:

<?php

$fn = ($p = key($_GET)) ? 'content/' . $p . '/index.php' : 'content/home/index.php';

(file_exists($fn)) ? require $fn : require 'content/404/index.php';

# end of file

Это простой роутинг для простых проектов. Достаточно создать в content каталог, как он станет доступен по одноименному адресу.

Related Posts
Comments (9) RSS
1 Прогер 88 2019-01-17 23:17:34

Круто, спасибо!


2 Александр 2019-02-05 10:45:58

Таки неплохо. Спасибо.


3 Богдан 2019-02-08 18:12:32

Объяснение так себе на тройку. В стиле "а вот есть такое, и есть такое", но деталей нет от слова вообще. Понять по этому тексту что куда и зачем можно только если уже знаешь все сам


4 Не богдан 2019-04-18 19:20:14
Объяснение так себе на тройку. В стиле "а вот есть такое, и есть такое", но деталей нет от слова вообще. Понять по этому тексту что куда и зачем можно только если уже знаешь все сам

Все четко написано!

А то нашел такую же статью просто выложил код и подписал:

код говорит сам за себя думаю не стоит объяснять!!!


5 Аноним 2019-07-31 08:31:30

Очень хороший материал и понятный сначала, но.... зачем в самом низу пример "Весь код в 2 строчки". Это же статья для новичков, понятно что вы крутой программист и можете все упаковать в 2 строки, но зачем усложнять, разложите на простые if(), пусть хоть 10 строк получится, а то ничего в конце не понял


6 Admin 2019-07-31 09:01:13 admin

Это сокращенная запись if/else. По идее даже новички их должны знать. Это же базовый синтаксис PHP. :-)


7 kubris 2019-12-12 14:59:52

Хорошая основа для новичков под дальнейший PHP обработчик:

//index.php :
$uri = $_SERVER['REQUEST_URI'];

if($uri == '/') {
    require 'pages/main.php';
    exit;
} else {
    echo '404 | ERROR'; //'pages/404.php';
    exit;
}

8 Alex 2020-02-26 11:09:24

Ну так как реализовать роутинг тот, что в Codeigniter ?


9 Admin 2020-02-26 15:06:17 admin

В CodeIgniter (и других фреймворках) два ключевых момента. Первый — шаблон url, на основании которого происходит сравнение текущего адреса. И второй — php-класс, который соответствует этому шаблону. Шаблон url по сути — обычная регулярка, когда она срабатывает, (авто)подключается php-класс с указанным методом.