Маршрутизация

Введение

Маршрутизация (роутинг) в ZoomX реализована с помощью библиотеки FastRoute. Она позволяет продвинутым разработчикам управлять процессом обработки запроса в привычной и удобной форме - через собственные контроллеры. Кроме того, маршрутизатор расширяет инструментарий работы с запросами и даёт возможность гибко работать с другими типами запросов - POST, PUT, PATCH и DELETE. Что открывает широкие возможности для построения REST архитектуры.

Режимы маршрутизации

В ZoomX есть 3 режима:

  • 0 – отключен. Все указанные роуты игнорируются.
  • 1 – смешанный (мягкий). Если для указанного URI роут не найден, то MODX продолжит обработку запроса в обычном режиме. Т.е. можно работать в режиме PHP шаблонизатора только с отдельными ресурсами.

    Если для указанного URI роут определён, но ресурса с таким URI нет, то ZoomX выведет страницу ошибки 404.

  • 2 – строгий (монопольный). Если для указанного 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');

Важно!

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

В роутах можно использовать плейсхолдеры (указываются в фигурных скобках) и регулярные выражения.

// Соответствует /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.

В качестве результата обработчик может вернуть различные типы данных:

  • Строку или число.
    $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');
});
Выделите опечатку и нажмите Ctrl + Enter, чтобы отправить сообщение об ошибке.