Как работает PHP-роутинг

Уж коли я затронул тему роутинга, то есть смысл немного окунуться в технические детали, поскольку большинство 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% их возможностей просто не используются.

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

Комментарий будет опубликован после проверки. Вы соглашаетесь с правилами сайта.

(обязательно)