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

PHP маршрутизатор (роутер)

10-04-2024Время чтения ~ 6 мин.PHP 713

Вопросы роутинга на PHP я уже рассматривал ранее, сейчас речь об его роли в архитектуре php-приложения. Достаточно долго я придерживался общепринятой точки зрения, что роутинг — это лишь та часть приложения, которая может быть заменена в любой момент. Сейчас же я считаю, что именно роутинг по сути и есть всё php-приложение.

Любое, абсолютно любое php-приложения, будь то простой сайт, фреймворк, CMS, вообще всё что угодно работает через роутинг. Даже если это статичные html-файлы — они всё равно работают через роутинг, просто этот роутинг обеспечивает Apache.

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

Шаблон URL по сути представляет собой паттерн для preg_match(). Он сравнивает этот паттерн с текущим URL и если совпадение найдено, то подключается указанный файл.

Многие могут возразить, что маршруты часто задаются как классы/методы/функции. Но на самом деле — это просто дополнительные параметры маршрута. Вначале нужно подключить файл с классом/функцией, а уже потом выполнить что-там нужно.

Роутинг (грамотного) PHP-приложения всегда должен строиться по следующему алгоритму.

Первое — это задать правила ЧПУ для .htaccess. В 99% случаев (для апача) это будет так:

Options +SymLinksIfOwnerMatch
Options -Indexes
DirectoryIndex index.php

<IfModule mod_rewrite.c>
RewriteEngine on
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . / [L,QSA]
</IfModule>

Если это подкаталог, то нужно его указать в RewriteBase и RewriteRule:

RewriteBase /subdir/
...
RewriteRule . /subdir/ [L,QSA]

Такой код гарантирует, что все запросы, кроме реальных файлов и каталогов будут отправлены на index.php. Таким образом этот файл всегда будет точкой входа в приложение. Уже в нём следует определить базовые константы приложения и подключить файл-загрузчик ядра (аля-bootstrap.php).

Второе — получить текущий URL. Это значит что нужно определить текущий адрес сайта — это особенно актуально для сайтов работающих в подкаталоге. Здесь же нужно определить разрешенные символы в URL — возможно они используются для XSS-атаки (можно сразу рубить такие запросы и перенаправлять на 404-страницу). Здесь же происходит декодирование через urldecode(), потому что адрес может содержать кодированные символы.

Нужно определить есть ли во входящем адресе $_GET. Если есть, то нужно его раскодировать на части по символу &. Нельзя использовать parse_str(), потому что она искажает данные. Дальше каждая часть рассматривается на наличие = и всё это в итоге оказывается в ассоциативном массиве, который потом добавляется в общий массив данных текущего URL.

Сам же URL декодируется через parse_url(). На выходе мы получаем массив с адресом относительно сайта, полный адрес, метод с учётом post-поля _method, query — это готовый массив из $_GET и массив сегментов из адреса делённый символом /.

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

Третий шаг — это непосредственный роутинг. Его задача сопоставить шаблоны маршрутов с текущим URL.

Здесь важно понять как именно будут составляться эти маршруты. Есть два принципиально разных подхода.

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

Route::get('/user', [UserController::class, 'index']); // Laravel
$routes->get('/', [Home::class, 'index']); // CodeIgniter 4
$r->addRoute('GET', '/user/{id:\d+}', 'get_user_handler'); // FastRoute

То есть в любом случае роутинг вначале определяет http-метод, а только потом сверяет шаблон регулярки.

Если использовать стандартные preg-шаблоны PHP, то всё сокращается до нескольких строчек кода:

// например $slug_pattern текущего роута равен 'page/([a-zA-Z]+)'
if (preg_match('~^' . $slug_pattern . '$~iu', CURRENT_URL['url'])) {
   // есть совпадение
   ...
}

Когда есть совпадение по шаблону, то смотрится его действие (action). Если это функция, то есть смысл сразу проверить, что она реально существует. Но всегда есть вероятность, что она есть, но ещё не загружена. Потому что в PHP нет механизма автозагрузки функций, наподобие автозагрузки классов. Хотя речь об этом идёт аж с 2011 года (https://wiki.php.net/rfc/autofunc). Но разработчикам PHP просто насрать на реальные задачи программистов. Их больше заботит как превратить язык в Яву.

Так вот, для автозагрузки функций, нужно предусмотреть какой-то свой механизм. Например их хранить в предопределённом каталоге, где каждая функция хранится в одноименном файле. В этом случае можно подгружать функцию, если она ещё не найдена. Если же и файла нет, то ставить флаг «is404», показывая, что для текущего URL не найдено реального соответствия.

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

На выходе роутинга должно быть имя исполняемой функции или класса/метода, а также флаг «is404», если ничего не найдено.

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

Намного лучший вариант — когда роутинг использует шаблоны, определенные не разработчиками (системы), а конечным пользователем.

Что такое php-приложение? Это всегда сайт, который выдаёт разные страницы по разным адресам. Даже если это json, xml и т.п.

Поэтому в идеале, конечный пользователь должен сам определять адрес страницы.

Например, если система использует базу данных, где есть таблицы с текстами записей, то поле «slug» будет хранить адрес страницы. Соответственно, роутинг будет делать sql-запрос к этому полю и если найдено, то будет отдавать соответствующую информацию, дескать данные, соответствующие этому URL, найдены.

В CodeIgniter работает т.н. автороутинг, который находит соответствие сегментам URL и имени класса (контролёра). Это не идеальный вариант, но он избавляет от необходимости где-то отдельно прописывать шаблоны маршрутов.

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

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

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

То есть после такого роутинга вопрос как именно выдать итог URL будет уже зависеть от задач системы.

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

Как я сделал роутинг в Albireo CMS? В параметрах записи есть поле slug. Вот с ним и сверяется текущий URL. Если нужен более сложный паттерн, то используется поле slug-pattern — это preg-регулярка. Таким образом пользователь сам определяет адрес и метод (поле method) на которые сработает данная страница.

Если же нужно подключить какой-то php-файл, то есть отдельный конфигурационный файл, который хранит массив паттернов маршрутов и полные имена файлов. Всё, что будет соответствовать текущему URL, будет автоматом подключено. Это позволяет использовать Albireo CMS даже вне «стандартного» варианта вывода страниц.


Слава Украине! Смерть рашистам!

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

Оставьте комментарий!