Маршрутизация
- Введение
- Режимы маршрутизации
- Использование
- Определение маршрутов
- Обработчики маршрутов
- Переадресация
- Автоматический детектор типа контента
- Другие HTTP методы
- RESTful API
- Виртуальные страницы
Введение
Маршрутизация (роутинг) в ZoomX реализована с помощью библиотеки FastRoute. Она позволяет продвинутым разработчикам управлять процессом обработки запроса в привычной и удобной форме - через собственные контроллеры. Кроме того, маршрутизатор расширяет инструментарий работы с запросами и даёт возможность гибко работать с другими типами запросов - POST, PUT, PATCH и DELETE. Что открывает широкие возможности для построения REST архитектуры.
Режимы маршрутизации
В ZoomX есть 3 режима:
- 0 – отключен. Все указанные роуты игнорируются.
- 1 – смешанный (мягкий). Работает и маршрутизация ZoomX и маршрутизация MODX. Если для указанного URI роут не найден, то MODX продолжит обработку запроса в обычном режиме. Т.е. можно работать в режиме PHP шаблонизатора только с отдельными ресурсами.
Если для указанного URI роут определён, но ресурса с таким URI нет, то ZoomX выведет страницу ошибки 404.
- 2 – строгий (монопольный). Работает только маршрутизация ZoomX. Если для указанного URI роут не найден, то обработка запроса будет завершена с ошибкой 404. Шаблонизатор MODX не запускается. Это условие справедливо, если включена автозагрузка ресурса.
Чтобы включить маршрутизацию, необходимо в системной настройке zoomx_routing_mode
указать значение 1 или 2.
Использование
Определение маршрутов
Маршруты (роуты) хранятся в файле core/config/routes.php
. В нём доступны 2 объекта - $router
и $modx
. Документация показывает нам 2 способа определения роута:
# 1. С помощью метода addRoute $router->addRoute('GET', '/users', 'handler'); # 2. С помощью коротких методов, соответствующих методу запроса HTTP $router->get('/users', 'handler');
Первый вариант удобно использовать для определения одного роута для нескольких методов запроса.
$router->addRoute(['GET', 'POST'], '/some-uri', 'handler');
Внимание!
С версии 3.5.0 URI в роутах могут начинаться со слэша. Раньше можно было указывать только URI, совпадающие с URI ресурса.
В роутах можно использовать плейсхолдеры (указываются в фигурных скобках) и регулярные выражения.
// Соответствует /user/42, но не /user/xyz $router->get('/users/{id:\d+}', 'handler'); // Соответствует /user/foobar, но не /user/foo/bar $router->get('/users/{name}', 'handler'); // Также соответствует /user/foo/bar $router->get('/users/{name:.+}', 'handler');
Плейсхолдеры в фигурных скобках передаются в хандлер в виде одноимённых параметров в том же порядке, в котором они объявляются в роуте.
Кроме того, части маршрута, заключенные в квадратные скобки ([...]), считаются необязательными, так что /foo[bar]
будет соответствовать как /foo
, так и /foobar
. Но такие части можно указывать только в конце роута.
// Этот роут $router->get('/users/{id:\d+}[/{name}]', 'handler'); // Равнозначен этим двум $router->get('/users/{id:\d+}', 'handler'); $router->get('/users/{id:\d+}/{name}', 'handler'); // Возможно также указать несколько необязательных частей $router->get('/users[/{id:\d+}[/{name}]]', 'handler'); // Неправильный роут - необязательная часть указана в середине роута $router->get('/users[/{id:\d+}]/{name}', 'handler');
Для более наглядной и удобной организации роуты можно группировать. Все роуты, определенные внутри группы, будут иметь общий префикс.
$router->addGroup('/admin', function (RouteCollector $r) { $r->get('/do-something', 'handler'); // admin/do-something $r->get('/do-another-thing', 'handler'); // admin/do-another-thing $r->get('/do-something-else', 'handler'); // admin/do-something-else });
Обработчики маршрутов
В качестве обработчиков маршрутов (хандлеров) можно использовать как анонимные функции так и контроллеры.
$router->get('/users/{id:\d+}', function($id) use($modx) { // Находим указанного пользователя $user = $modx->getObject('modUser', ['id' => $id]); // Принудительно указываем ресурс, который отвечает за вывод профиля пользователя. $modx->resourceIdentifier = 5; return viewx('profile.tpl', $user->toArray()); });
Но принятая практика - это использование контроллеров. Это позволяет сгруппировать всю бизнес-логику работы с предметной областью в одном месте. А в случае с FastRoute ещё и использовать кэширование роутов (пока не реализовано).
// Список пользователей $router->get('/users', ['Zoomx\Controllers\UserController', 'index']); // Данные конкретного пользователя $router->get('/users/{id:\d+}', ['Zoomx\Controllers\UserController', 'show']); // Добавить пользователя $router->post('/users', ['Zoomx\Controllers\UserController', 'create']);
Метод index
можно опустить - достаточно указать только класс контроллера.
$router->get('/users', Zoomx\Controllers\UserController::class);
Контроллеры находятся в папке core/components/zoomx/src/Controllers/
. Пользовательский контроллер можно разместить в ней же. Этот контроллер должен расширять базовый контроллер ZoomX\Controllers\Controller
.
<?php namespace Zoomx\Controllers; class UserController extends Controller { public function index() { return viewx('users.tpl'); } public function show($id) { // Можно принудительно указать ресурс для вывода профиля пользователя - $modx->resourceIdentifier = 5; // Или указать, что это виртуальная страница zoomx()->autoloadResource(false); // Находим пользователя. $user = $this->modx->getObject('modUser', ['id' => $id]); return viewx('profile.tpl', $user->toArray()); } }
Внимание!
В некоторых примерах для вывода на страницу данных пользователя используется такой код $user->toArray()
. Необходимо знать, что класс modUser содержит конфиденциальную информацию. Поэтому в рабочем приложении перед выводом данных пользователя необходимо такую информацию удалить.
Важно понимать, что для указанных в примерах роутов должны существовать ресурсы с соответствующими URI.
Если все контроллеры находятся в одном пространстве имён, то его можно опустить и писать только имена классов. Для этого в системной настройке zoomx_controller_namespace
нужно указать нужный неймспейс контроллеров. Он будет автоматически добавлен при обработке. По-умолчанию в этой настройке указано пространство имён "Zoomx\Controllers".
$router->get('/uri', ['MyController', 'method']); // Преобразуется в Zoomx\Controllers\MyController
Если нужно указать контроллер из другого пространства имён, то первый символ в имени должен быть обратный слеш. В этом случае базовое пространство имён добавляться не будет.
$router->get('/uri', ['\MyController', 'method']);
В качестве результата обработчик может вернуть различные типы данных:
- Строку или число.
$router->get('/hello.html', function() { return "Привет!"; });
- Массив. Он будет автоматически преобразован в JSON формат и ответ вернётся с правильным типом контента
application/json
.$router->get('/data', function() { return ['foo' => 'bar', 'key' => 'value']; });
- Объект класса Zoomx\View или его псевдоним ZoomView.
$router->get('/hello.html', function() { // Первый параметр - название файла шаблона, второй - данные для шаблона. return new ZoomView('hello.tpl', ['name' => 'John']); });
- Функцию-хелпер
viewx()
, возвращающую объект класса ZoomView.$router->get('/hello.html', function() { // Первый параметр - название файла шаблона, второй - данные для шаблона. return viewx('hello.tpl', ['name' => 'John']); });
- Функцию-хелпер
jsonx()
, возвращающую объект класса Zoomx\Json\Response. Преимущество данного способа перед простым возвратом массива - возможность добавить в ответ HTTP заголовки.$router->get('/hello.html', function() { // Первый параметр - массив данных, второй - массив пользовательских HTTP заголовков. return jsonx(['foo' => 'bar'], ['CustomHeader' => 'Value']); });
- Функцию-хелпер
redirectx()
, упрощающую переадресацию.$router->get('/first.html', function() { // Вместо // $modx->sendRedirect('second.html', ['responseCode' => $_SERVER['SERVER_PROTOCOL'] . ' 301 Moved Permanently']); return redirectx('second.html', 301); });
- Функцию-хелпер
filex()
, позволяющую вернуть файл или для отображения или для скачивания.$router->get('/files/{file}', function($file) { // Отключить автозагрузку ресурса zoomx()->autoloadResource(false); // Второй параметр отвечает за режим скачивания return filex(MODX_CORE_PATH . 'path/to/users/files/' . basename($file), true); });
Переадресация
В некоторых случаях может потребоваться переадресация. Самый привычный вариант - использовать стандартный метод modX::sendRedirect()
.
$router->get('/product1.html', function() use($modx) { $modx->sendRedirect('catalog/product2.html'); });
Более функциональный вариант переадресации - использование специальной функции-хелпера redirectx(). Она позволяет указать код переадресации во втором параметре. В третьем параметре можно указать массив дополнительных HTTP заголовков.
$router->get('/first.html', function () use ($modx) { // Вместо $modx->sendRedirect('second.html', ['responseCode' => $_SERVER['SERVER_PROTOCOL'] . ' 301 Moved Permanently']); return redirectx('second.html', 301, ['X-Custom-Header' => 'Some value']); });
Есть ещё вариант - переопределить переменную $modx->resourceIdentifier
, указав в ней или id или URI нужного ресурса. В этом случае для указанного URI будет выведен другой ресурс. Но подмена для пользователя будет незаметна, так как не будет соответствующего кода HTTP. И адрес в браузере не изменится. Это больше похоже на modX::sendForward()
.
// Использование идентификатора ресурса $router->get('/page1.html', function() use($modx) { // можно указать id $modx->resourceIdentifier = 2; // или URI ресурса $modx->resourceIdentifier = 'another_page.html'; return viewx('page.tpl', ['foo' => 'bar']); });
Важно понимать!
Так как маршрутизатор срабатывают в самом начале запроса, то в роутах ресурс ещё не определён ($modx->resource = null
). Менеджер запроса подгрузит его позже. Но если вам очень нужен ресурс именно в роуте, то придётся самостоятельно его запросить. В этом случае автоматический поиск задействован не будет.
Пример, аналогичный предыдущему, но с поиском ресурсов без расширения html
.
$router->get('/blog/{alias}.html', function($alias) use($modx) { $modx->resource = zoomx()->getResource('blog/' . $alias); // Без расширения html return viewx('article.tpl'); });
Последний вариант позволяет проксировать запросы к статическим файлам. Это может понадобиться для проверки прав доступа, подсчёта количества скачиваний, промежуточных вычислений перед отдачей. Например можно распарсить JS файл, чтобы инициализировать нужные объекты или конфиги. Примеры можно посмотреть в документации к функции filex().
Автоматический детектор типа контента
В некоторых случаях нужно вернуть контент с типом, отличным от text/html
или application/json
. Например, нужно отдать pdf-файл или вернуть данные в xml формате. Особенно это актуально для виртуальных страниц или в режиме API. Т.е. когда нет созданного ресурса с настроенным типом содержимого. В этом случае нужно указать заголовок Content-Type
с нужным типом. Сделать это можно или напрямую указав его с помощью функции header()
или указав расширение в URI.
$router->get('/data.xml', function() { // Отключить автозагрузку ресурса zoomx()->autoloadResource(false); return viewx('xml/data.tpl'); });
Другие HTTP методы
К сожалению формы умеют работать только с методами GET и POST. Для того, чтобы использовать другие HTTP методы, необходимо включить системную настройку zoomx_http_method_override
(по-умолчанию, включена) и добавить в форму скрытый input с именем "_method" и названием метода.
<form> <input type="hidden" name="_method" value="PUT"> ... </form>
После этого будет работать следующий роут
// Обновление пользователя $router->put('/users/{id:\d+}', ['Zoomx\Controllers\UserController', 'update']); // в методе update нужно указать параметр $id
В большинстве случаев в обычном режиме с методами PUT, PATCH и DELETE работают редко. В основном они используются в API режиме.
RESTful API
Кроме управления шаблонами FastRoute предлагает полноценный механизм RESTful API. В MODX уже есть встроенный функционал поддержки этого архитектурного стиля. Но он крайне ограничен и отстаёт от современных стандартов. Он требует создание отдельной точки входа и может работать только с одной сущностью.
Принцип построения REST запросов
Метод HTTP | URI | Метод контроллера | Описание |
---|---|---|---|
GET | /users |
index | Вывести список пользователей. |
GET | /users/create |
create | Вывести форму создания пользователя. |
POST | /users |
store | Добавить пользователя в БД. |
GET | /users/{id} |
show | Вывести данные указанного пользователя. |
GET | /users/{id}/edit |
edit | Вывести форму редактирования пользователя. |
PUT/PATCH | /users/{id} |
update | Сохранить изменения в БД. |
DELETE | /users/{id} |
delete | Удалить указанного пользователи из БД. |
Использовать RESTful API можно как с помощью обычных веб запросов (через формы), так и с помощью асинхронных запросов (через API). Принцип работы через формы описан выше. Второй способ реализуется с помощью AJAX технологии и подразумевает обмен данными в JSON формате. Для обработки таких запросов используются отдельные классы, в которых исключены лишние проверки и обработки, использующиеся в обычном режиме. В режиме API нет автоматического определения ресурса по URI. Запрос обрабатывается в контроллере или анонимной функции роутера и результат сразу возвращается обратно клиенту. Таким образом, скорость обработки запроса существенно повышается. Данный режим включается автоматически при наличии заголовка Accept
со значением application/json
.
Но режим API можно включить и в роуте. Для этого надо или вернуть массив или функцию-хелпер jsonx()
.
$router->get('/api/posts', function() { $posts = get_posts(); ... return jsonx($posts); });
В данном случае есть небольшой оверхед, так как для обработки запроса используется обычный класс Request
, в отличие от более лёгкого Json\Request
.
Виртуальные страницы
Специфика MODX состоит в том, что для корректной работы в системе обязательно должен быть определён ресурс. Поэтому даже для виртуальной страницы необходимо подсовывать пустой объект ресурса. В режиме виртуальной страницы поиск ресурса по указанному URI отключается. Что с точки зрения оптимизации гораздо лучше, чем сначала искать в БД ресурс, а затем перенаправлять пользователя на страницу ошибки.
Для работы в режиме виртуальной страницы нужно отключить автозагрузку ресурса в системной настройке zoomx_autoload_resource
. Но это действие переводит сайт в режим фреймворка - тогда нужно в роутах самостоятельно искать ресурс. А чтобы отключить автозагрузку ресурса только для определённых маршрутов, нужно в них отключить эту настройку с помощью специального метода главного сервиса zoomx()->autoloadResource(false)
. Такие маршруты можно группировать, чтобы не писать в каждом.
$router->addGroup('/files', function (RouteCollector $r) { zoomx()->autoloadResource(false); $r->get('/do-something', 'handler'); $r->get('/do-another-thing', 'handler'); $r->get('/do-something-else', 'handler'); });