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

Виды PHP-роутинга

01-05-2020Время чтения ~ 7 мин.PHP 3838

Фактически любой php-проект содержит какой-то роутинг. Без роутинга могут обходиться наверное только единичные страницы, где используется индексный файл index.php (например — лендинг), а также прямое обращение к php-файлу на сервере (сайт/contact.php и т.п.). Также встречаются разработки (обычно старые), где роутинг вынесен в .htaccess, в котором явно прописывается соответствие входящего URL какому-то php-файлу на сервере.

Но на сегодняшний день фактически стандартом стал подход, когда в .htaccess задаются простые правила, по которым все входящие запросы передаются во фронт-контролер (обычно это index.php). После этого загружается «ядро», может быть происходит какая-то инициализация, и дальше управление передаётся в роутер. Роутер на основе заданных правил подключает нужные файлы и, например, запускает на выполнение соответствующие функции/методы. Если бы мы рассматривали минимальное php-приложение (по современным подходам), то оно бы состояло только из фронт-контролера и роутера. Всё остальное — это обвеска на роутер. Именно так устроены современные php-фреймворки.

Про роутинг я ранее уже рассказывал больше с технической точки зрения. Теперь рассмотрим роутинг с архитектурной.

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

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

В некоторых случаях используется автоопределение класса/метода (как в CodeIgniter), но по сути это тот же самый вариант, где действует некое соглашение: URL соответствует какому-то php-классу (с учётом его автозагрузки по PSR-4).

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

Второй вариант условно можно назвать автороутингом. В его работе не требуется прописывать правила для каждой страницы, а сами страницы строятся по какому-то единому принципу. Автороутинг характерен для сайтов, где используется множество страниц с произвольными адресами.

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

Поэтому автороутинг базируется на каких-то заранее определённых правилах.

Наверное самый простой роутинг делается на файлах или каталогах. Например в Landing Page Framework каждый адрес соответствует каталогу (в определённом месте). Например если нужно сделать страницу сайт/about, то создаётся каталог lpf-content/pages/about.

Можно сказать, что автороутинг CodeIgniter работает по похожей схеме: достаточно разместить файл контролёра, совпадающий с адресом. Правда здесь возникнет сложность с многоуровневыми адресами. Например в LPF для адреса сайт/about/contact достаточно повторить эту же структуру в виде каталогов: lpf-content/pages/about/contact, а в CodeIgniter все обращения по любому попадут в контролёр About.php.

Предопределённые файлы/каталоги достаточно простой вариант роутинга. Задача значительно усложняется, если мы работаем с базой данных, где адрес страницы хранится в каком-то отдельном поле.

В теории кажется что всё просто. Поле хранит адрес страницы (назовём его slug). Роутинг делает запрос к базе и находит соответствие с входящим URL. Если есть, подключает файл для вывода, если нет, то отдает 404-страницу.

На практике, однако, встречается масса подводных камней. Скажем нужна проверка на дублирующиеся адреса, поскольку не должно быть несколько разных страниц по одному адресу. Поэтому при публикации slug нужно прогонять через фильтры, чтобы адрес оказался корректным и уникальным.

Но главную сложность добавляет многочисленные виды контента: рубрики, метки, записи, авторы, пользователи и т.п. То есть необходим механизм, который позволил бы исключать дублирующиеся адреса из разных таблиц базы. Скажем может быть страница со ссылкой news, но точно также может быть и рубрика news. А через какое-то время редактор решит добавить и метку news. Все они используют один и тот же адрес сайт/news — как роутер должен разрешать такой конфликт?

Для решения подобных проблем используются разные подходы. Один из них — задействовать т.н. таксономию (taxonomy). Если утрировать, то это несколько таблиц, которые хранят связи между собой и таблицами данных.

Работает это так. При создании записи получается её id. Этот id прописывается в таблицах таксономий, где хранится уникальное поле slug. Создание рубрики происходит точно также: id рубрики прописывается в таблицах таксономий и там же её slug. Таким образом slug оказывается уникальным, что исключает конфликт адресов.

Таксономия достаточно сложный вариант не только для понимания, но и реализации. Кроме того это порождает достаточно сложные SQL-запросы к базе. Поэтому таксономия нужна только для проектов, где предполагается множество «сущностей»/«типов объектов». Из CMS таксономия используется в Drupal и примитивной форме в ВП.

Более простой вариант — создание таблицы, где хранятся все slug'и сайта. Обычно это т.н. meta-таблицы, которые хранят данные для других таблиц. Работает примерно как таксономия, но поскольку связей между таблицами («сущностями») нет, то достаточно одной такой slug-таблицы. Роутер находит в ней адрес (slug) и получает исходную таблицу (или тип данных), например «page», «category», «tag» и т.п., и id записи в этой таблице.

Когда адрес хранится в отдельной таблице и связан через id с другой, это порождает серьёзную проблему переноса и целостности данных. То есть чтобы создать запись нужно получить её id (а он может поменяться при переносе), и только потом можно его связать с второй slug-таблицей.

С таксономией проблемы аналогичны.

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

Следующий вариант базируется на том, что существуют какие-то служебные адреса, которые нельзя использовать при создании записей. Обычно это префиксы (первый сегмент URL): сайт/category/news, сайт/tag/news. В данном случает category и tag нельзя использовать при создании записей, хотя для обычной записи можно использовать сайт/news. Во всех этих случаях один и тот же slug (news), но за счёт префиксов получаются разные URL.

При желании можно вынести настройки служебных префиксов, что позволит их менять в любом варианте. Но в любом случае такой вариант базируется на частичном обращении роутера к базе данных, поскольку когда встречается служебный сегмент, то это сразу делает очевидным какой файл следует подключать. Если же такого нет, то уже проверяется slug в таблице записей. А вот если и её нет, то это уже 404-страница.

Похожим вариантом будет жесткая привязка адреса к первому сегменту. Такой подход реализован в MaxSite CMS, где первый сегмент отвечает за тип выводимых данных. Преимуществом такого подхода является то, что роутер вообще не обращается к базе и определяет подключаемый файл прямо по URL.

Большой плюс такого подхода — в высокой скорости работы сайта. Кажется, что пара-тройка дополнительных запросов не сыграют большой роли, но на практике это создаёт дополнительные проблемы с нагрузкой на сервер. Нюанс ещё в том, что при проектировании такого сайта допускается предположение, что данные нужно получать только один раз (что само по себе верно), а поскольку это происходит в роутере, то роутер берёт на себя ещё и функцию получения данных до того, как управление передано в контролёр или файл. Это логическая ошибка стоит того, что такие сайты начинают сильно тормозить и нагружать сервер. Ну, думаю, владельцы сайтов на Drupal и ВП в курсе о чём речь. :-)

Поэтому, самым предпочтительным вариантом и является роутинг на анализе URL.

Существует ещё один вариант роутинга, который очень популярен для высокопосещаемых сайтов, вроде новостных. Адреса записей формируются со служебным префиксом, например «news», после чего идёт id записи и slug, примерно так: сайт/news/1234-super-puper-novost.

Роутинг, когда находит подобный адрес, из последнего сегмента вычленяет только число, которые не что иное как id записи («1234»). То есть часть «super-puper-novost» может быть абсолютно любой — роутер её просто игнорирует.

ps Подписывайтесь на мой telegram-канал, где я пишу о PHP и создании сайтов!

Похожие записи