Как работает PHP-роутинг
02-05-2020Время чтения ~ 7 мин.PHP 9241
Уж коли я затронул тему роутинга, то есть смысл немного окунуться в технические детали, поскольку большинство php-библиотек для роутинга представляются загадочными и сложными не только для новичков, но и опытных специалистов. Проблема здесь в том, что каждый разработчик пытается реализовать свои идеи, которые, как он думает, должны подходить для всех и каждого.
Данный подход делает php-разработки сложными и запутанными. PHP — такой язык программирования, который позволяет решать задачи просто и понятно. Во многих случаях не нужна лишняя обвеска и дополнительный уровень абстракции, которая только запутывает код. Работа с HTTP по какой-то мистической причине, часто обвешивается тонной абстракций, хотя всё крутится вокруг простых вещей.
Каркас приложения
Современное php-приложение строится достаточно просто. Все http-запросы отправляются в одну точку входа — это фронт-контролер. Обычный index.php в корне сайта. ЧПУ организуется в .htaccess. Приведу пример файла из одной своей разработки:
Options +FollowSymLinks Options -Indexes DirectoryIndex index.php AddDefaultCharset UTF-8 <IfModule mod_rewrite.c> RewriteEngine on RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . / [L,QSA] </IfModule>
В прошлой статье я уже рассказывал как это работает: всё передаётся в индексный файл (index.php).
Во фронт-контролере подключаются необходимые php-библиотеки, настраивается автозагрузка классов PSR-4 и определяются базовые константы (об этом я рассказывал в своём telegram-канале, поэтому не буду повторяться), например BASE_DIR
, SITE_URL
, SITE_PROTOCOL
, SITE_HOST
и т.д.
И после этого управление передаётся в точку входа application, то есть там, где непосредственно размещается ваше приложение. Например это будет файл app/bootstrap.php
. Уже в этом файле запускается роутинг.
Следует отметить, что во многих php-фреймворках роутер подключается скрыто где-то в дебрях своих библиотек и модулей. Пользователю остаётся только возможность конфигурации через отдельные файлы. Полноценное управление роутером здесь невозможно. И уж тем более сменить его на какой-то свой вариант.
Запуск роутера
Поскольку app/bootstrap.php
является точкой входа для приложения, то в нём и размещается роутер.
Роутер — это самый обычный php-класс, который может размещаться как угодно. Главное, чтобы он соответствовал PSR-4.
Первым делом создаём объект роутера. После этого считываем конфигурацию, где хранится массив правил для роутинга, скажем файл app/Config/routes.php
.
Как хранить опции и конфигурации я также рассказывал недавно в своём telegram-канале.
Этот массив мы добавляем как правила роутинга. При этом мы можем прочитать и множество других файлов с правилами, для того, чтобы разделить их по модулям своего приложения, а не кидать всё в одну кучу. Правила роутинга суммируются и накапливаются в его объекте.
После этого можно задать специальное правило для случаев, если никакие другие не сработали. Это будет 404-страница, а точнее его класс и метод.
Дальше запускаем роутинг, который сам уже разруливает правила и подключает нужные классы. В некоторых php-фреймворках объект роутера даже именуется как $app
, что показывает его основное назначение — запуск приложения.
В коде это выглядит так:
// создание объекта $route = new \Lib\Router\Router; // подключаем правила из конфига if ($rules = \Lib\Core\Core::readConfig('routes.php')) $route->addRules($rules); // данные для 404-страницы $route->setNotFound('\Modules\Page404\Page404@index'); // run application $route->run();
Правила роутинга
Конфигурационный файл возвращает массив, где каждое правило формируется тоже как массив. Просто покажу пример в котором это хорошо видно:
return [ [ 'method' => 'GET', 'pattern' => '', // home 'action' => '\Modules\Home\Controller@index', ], [ 'method' => 'POST', 'pattern' => '', // home 'action' => '\Modules\Home\Controller@post', ], // contact использует класс Pages [ 'method' => 'GET', 'pattern' => 'contact', 'action' => '\Modules\Pages\Pages@contact', ], [ 'method' => 'POST', 'pattern' => 'contact', 'action' => '\Modules\Pages\Pages@contactPost', ], ];
Три основных ключа любого правила:
- method — указывает на http-метод (GET, POST, OPTION, MYMETHOD). Любой вариант, можно и свой придумать. Для любого варианта используется ANY.
- pattern — паттерн адреса. Представляет собой обычную регулярку. В роутере можно предусмотреть спецзамены для более простого использования ([:any], [:num], [:segment] и т.д.).
- action — действие. Класс и метод, которые сработают на этот паттерн. Это может быть
класс@метод
, просто функция (function), либо название функции.
Кроме этого иногда нужно передать в action какие-то данные прямо из правила. В этом случае используется ключ param
, где указывается, то что будет востребовано в выполняемом классе. Но, опять же это особые ситуации.
Лично я предпочитаю для конфигурации использовать именно php-массив, но многие роутинги позволяют добавлять эти же самые правила через строчку. Она парсится и добавляется во внутреннее поле объекта уже как положено в массиве.
В общем не важно как именно добавляются правила в итоге они всё равно должны описывать http-метод, URL-паттерн и действие для выполнения.
Метод setNotFound()
описывает только action в том же формате.
Логика работы роутера
Запуск роутера происходит через метод run()
. Первое, что там происходит — это сравнение текущего URL с паттернами правил. Например это может быть приватный метод match()
, который возвращает результат сравнения.
В match()
проверяется не только паттерн (это обычная регулярка), но и http-метод. Чтобы это сделать для текущего URL формируется массив его данных, который удобен для дальнейшего использования. Вот что-то такое:
[method] => GET [method_data] => Array () [url] => [url_full] => http://сайт/ [query] => [url_segment] => Array ( [0] => ) [query_data] => Array ()
Разбор URL в большинстве случаев это не что иное, как данные из $_SERVER
. В некоторых случаях ещё используют parse_url()
и parse_str()
для корректного декодирования адресов. Но в большинстве случаем роутинг любого php-проекта работает именно на этих трёх компонентах. Всё, что сверх этого — бессмысленная абстракция и утяжеление кода.
Когда match()
находит совпадение по http-методу и паттерну, он парсит строчку action
. На выходе получаем (в run()
) массив результата проверки, где уже подключаем файл, выполняем функцию или инициализируем объект указанного класса и выполняем указанный метод.
Если же возникла ситуация, что ни один action
не сработал, то выполняем указанный для 404-страницы метод. Хотя, если и он не найден, то просто ничего не делаем. Роутер может вернуть false
, который уже анализируется в app/bootstrap.php
.
if (!$route->getResult()) echo '404-page';
Такой вариант позволяет не задавать 404-страницу для роутера.
Задача любого роутера — найти соответствие URL какому-то паттерну и запустить в случае успеха «что-то». Больше в его задачу ничего не входит, поскольку это уже лишнее.
Точно также, как и лишними будут многочисленные функции вроде getPost()
, getGet()
— возвращающие стандартные $_POST
и $_GET
. Разработчики PHP очень упростили задачу по работе с этими http-методами (включая и $_FILES
) — подменять их своими функциями — больше смахивает на глупость.
HTTP-методы
В заключении пару слов от http-методах. На практике есть только два варианта: POST и GET. Именно их браузеры и поддерживают, хотя есть некие стандарты, которые предполагают существование других вариантов. Кроме того эти методы завязаны на HTML — в основном это обычные формы, либо Ajax-запросы. Во всех таких случаях, как ни крути, в реальности используется только POST.
Чтобы ничего не ломать, придумали хитрость: в post-запросе отправлять поле _method
, который и содержит название http-метода. Для формы это выглядит так:
<form method="POST"> <input type="hidden" name="_method" value="PUT"> ... прочие данные формы ... </form>
То есть реальная отправка — POST, но роутер проверят наличие поля _method
. Именно поэтому http-метод в php-роутере может быть абсолютно любым.
На этом базируется концепция RESTful и CRUD, где используется один адрес, но разные http-методы. Это указывается в правилах роутера:
[ 'method' => 'POST', 'pattern' => 'task', // create new task (Create) 'action' => '\Controller\Task\Task@create', ], [ 'method' => 'GET', 'pattern' => 'task', // task show (Read) 'action' => '\Controller\Task\Task@read', ], [ 'method' => 'PUT', 'pattern' => 'task', // update/edit task (Update) 'action' => '\Controller\Task\Task@update', ], [ 'method' => 'DELETE', 'pattern' => 'task', // delete task (Delete) 'action' => '\Controller\Task\Task@delete', ],
А в форме что-то вроде такого:
... <button type="submit" name="_method" value="PUT">Update selected</button> <button type="submit" name="_method" value="DELETE">Delete selected</button>
В зависимости от сложности проекта, создаётся и роутинг. С моей точки зрения лучшим вариантом будет именно свой «велосипед», поскольку он на 100% покроет реальные задачи. Сторонние библиотеки всегда стараются сделать универсальными, а это приводит к тому, что 80% их возможностей просто не используются.