Собственная маршрутизация в MODX

Есть очень частый вопрос "а как вывести то-то по такой-то ссылке?". И речь здесь не о заморозке uri, а о собственном маршруте нахождения страницы по указанному адресу, типа такого. То есть, речь идёт о маршрутизации, или, в народе - роутинге.

MODX отлично работает с дружественными url и представляет кучу настроек для их функционирования, а там, где настроек не хватает, он даёт нам замечательный инструмент для описания собственной логики - системные плагины по событию OnPageNotFound.

Как следует из названия, событие это возникает, когда MODX не смог найти страницу по запрошенному адресу, и в этот момент мы можем перехватить запрос и вывести что-то своё. Если же мы ничего не перехватываем, то выводится 404 not found.

Теория

За обработку запроса от пользователя отвечает класс modrequest.class.php, в котором запускается метод handleRequest(). Он определяет, каким именно образом запрашивается страница, по id или по alias?

Первый случай нас не интересует, ибо это не friendly urls, а вот во втором вызывается сравнительно новый метод modX::findResource(). Именно он отвечает за сопоставление запрошенного адреса и id страницы.

Если страница найдена, то findResource возвращает её id, MODX получает нужный объект, передаёт в класс ответа modresponse.class.php и всё хорошо. А вот если же findResource возвращает false, то вызывается метод modX::sendErrorPage().

Именно в этом методе и генерируется событие OnPageNotFound, в которое мы можем вклиниться и подсунуть нужную нам страницу.

Практика

Пишем плагинчик:

<?php
if ($modx->event->name != 'OnPageNotFound') {
    return false;
}

echo '<pre>';
print_r($_REQUEST);
print_r($_SERVER);
die;

Отмечаем для него соответствующее событие, сохраняем и открываем на сайте любой несуществующий адрес, типа site.com/notfoundpage.html.

Вы должны будете увидеть вот такой вывод:

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

Грамотно ловим запрошенный юзером адрес:

// Определяем системную переменную friendly urls
$alias = $modx->context->getOption('request_param_alias', 'q');

// Проверяем её наличие в запросе.
// Если не находим, то это фигня какая-то - выходим
if (!isset($_REQUEST[$alias])) {return false;}
// А если есть - работаем дальше
$request = $_REQUEST[$alias];

echo $request;die;

Вот мы и получили запрошенный адрес. Собственно, главный фокус в том, чтобы опираясь на запрошенный адрес вывести нужную нам информацию.

Давайте продолжим на реальном примере.

Вывод товаров бренда

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

[[!mFilter2?
    &parents=`9`
    &hideContainers=`1`
    &element=`msProducts`
    &filters=`
        ms|price:number,
        parent:parents,
        msoption|tags,
        ms|vendor:vendors
    `
    &class=`msProduct`
    &sort=`product|pagetitle:asc`
    &tplFilter.outer.ms|price=`tpl.mFilter2.filter.slider`
    &tplFilter.row.ms|price=`tpl.mFilter2.filter.number`
    &tpls=`tpl.msProducts.row,tpl.msProducts.row2`
]]

Затем разбрасываем на сайте ссылки типа /brands/имябренда/, или /brands/имябренда.html, или /brands/имябренда - на ваше усмотрение. Мне нравится /brands/имябренда.

Пройдя по такой ссылке юзер сгенериует событие OnPageNotFound, где его перехватит наш плагин. Проверяем:

Ну а теперь можно уже и написать рабочий плагин:

<?php
if ($modx->event->name != 'OnPageNotFound') {return false;}
$alias = $modx->context->getOption('request_param_alias', 'q');
if (!isset($_REQUEST[$alias])) {return false;}

$request = $_REQUEST[$alias];
$tmp = explode('/', $request);
// Ссылка подходит под заданный формат: brands/brandname
if ($tmp[0] == 'brands' && count($tmp) >= 2) {
    // Определяем id раздела /brands/.
    // Конечно, можно его и руками прописать - но так гибче
    if (!$section = $modx->findResource($tmp[0] . '/')) {
        // Если вдруг раздел куда-то делся - выходим.
        return false;
    }
    // Теперь очищаем имя бренда от возможного расширения
    $name = str_replace('.html', '', $tmp[1]);
    // Если очищенное имя не равно запрошенному - то можно отредиректить юзера
    // Также возможен вариант с косой на конце имени бренда - его тоже учитываем
    // SEOшники должны оценить =)
    if ($tmp[1] != $name || (isset($tmp[2]) && $tmp[2] == '')) {
        $modx->sendRedirect($tmp[0] . '/' . $name);
    }
    
    // Люди с неправильной ссылкой ушли на правильную и дошли до этого момента со второго раза
    // Дальше проверяем наличие запрошенного бренда
    if ($brand = $modx->getObject('msVendor', array('name' => $name))) {
        // Круто, такой бренд есть, получаем его id
        $id = $brand->get('id');
        
        // Осталось выставить нужные переменные в запрос, как будто юзер их сам указал
        // Так как это mFilter2 - выставляем выбранный бренд
        $_GET['ms|vendor'] = $_REQUEST['ms|vendor'] = $id;
        // А теперь подсовывем юзеру страницу брендов, а дальше сниппет на ней сам разберётся
        $modx->sendForward($section);
    }
}
// Иначе ничего не делаем и юзер получает 404 или его перехватывает другой плагин.

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

Вот и вся маршрутизация. У кого остались вопросы?

← Предыдущая заметка
AjaxSnippet и аякс пагинация
Комментарии (22)
ВолодянВолодя
27.03.2014 21:22

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

bezumkinВасилий Наумкин
27.03.2014 21:26

А это уже особенности работы сниппета, который вызван на странице. Замени

$_GET['ms|vendor'] = $id;

на

$_GET['ms|vendor'] = $_REQUEST['ms|vendor'] = $id;
ВолодянВолодя
27.03.2014 21:37

понял! Спасибо

Ilya Ev
27.03.2014 21:39

Был тоже вопрос)) с поправкой все заработало, спасибо большое... Доеду до дома скину спасибу)

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

bezumkinВасилий Наумкин
27.03.2014 21:42

Только внешний вид, а работать должно.

Ilya Ev
27.03.2014 21:43

да верно, работает только вид). Так и хочется еще одно поле с транслитом для брендов присобачить))

bezumkinВасилий Наумкин
27.03.2014 21:46

Ты можешь выводить ссылки на бренды через фильтр вывода, который будет их транслитирировать, а в плагине делать обратную транслитерацию.

Ну или использовать одно из ненужных полей msVendor - их там много.

Ilya Ev
27.03.2014 21:49

о точно, ненужные поля)) Туда и записать красивые url и теперь вообще не придраться к ссылкам в разделах)

Еще раз спасибо.

ВолодянВолодя
27.03.2014 21:55

с русскими буквами все нормально...вместо пробела %20 подставляется

Sergey Startsev
17.06.2014 17:11

Василий, подскажи где можно почитать про findResource()? Документацию не нашел Пытаюсь подставить $alias или $uri, но результат всегда fasle

bezumkinВасилий Наумкин
17.06.2014 17:20

В исходном коде, где ж еще? Держи ссылку.

Sergey Startsev
17.06.2014 22:04

Спасибо, проблема была в контексте, по исходнику понятно стало, что его можно передать.

Сергей Малышев
24.07.2014 02:31

Василий, есть проблема, связанная с маршрутизацией. Нужно, чтобы одновременно существовал статичный сайт и modx. Сайт и modx установлены локально на OpenServer в папку site.loc. В корневой папке статичного сайта есть свой маршрутизатор - router.php. В htaccess (modx) внесены изменения, для того, чтобы он направлял все запросы на router.php, а не на index.php, как было бы при установке только modx. router.php, если приходит несуществующий URL, осуществляет редирект на index.php (modx) через header('Location: http://site.loc/index.php');. Но modx выводит не 404-ю страницу а Главную. При исходном .htaccess, который отправляет запросы на index.php, все нормально, отображается 404-я страница. Не пойму, почему так происходит. замена header() на require() тоже ничего не дают. Непонятно, как делать 2-х ступенчатую маршрутизацию. Помогите пожалуйста. Подскажите, в каком направлении копать.

bezumkinВасилий Наумкин
24.07.2014 02:33

Не подскажу - не использую ни OpenServer, ни Apache2.

Сергей Малышев
24.07.2014 02:43

OpenServer ни при чем. Я думаю, тоже самое будет и на нормальном хостинге. Дело в принципе. Как сделать 2-х ступенчатую маршрутизацию, чтобы index.php понимал, что пришел несуществующий URL?

bezumkinВасилий Наумкин
24.07.2014 02:55

Давай подумаем...

Может, нужно передать ему какой-то параметр или заголовок? Например, адрес исходного запроса, который router.php не смог обработать?

Сергей Малышев
24.07.2014 03:02

Рою в интернете. Наверное быстро не разобраться. Если разберусь, то напишу. Нужно решение для постепенного переноса статичного сайта на modx. Если не получится, буду делать импорт сайта и одним махом заменю, но здесь есть риск потери позиций сайта, Может посоветуете, как постепенно перенести сайт на modx?

Сергей Малышев
24.07.2014 13:49

Да, Вы были правы. Нужно было передавать роутеру на index.php параметр. помогло: header('Location: http://site.loc/index.php?q='.$\_SERVER\['REQUEST\_URI'\]); Спасибо за подсказку. Буду постепенно(!) переводить сайт на modx.

bezumkinВасилий Наумкин
24.07.2014 14:36

Удачи!

kondakovДмитрий Кондаков
06.08.2014 14:09

Василий возник вопрос, а как такой виртуальной странице передать свой pagetitle?

bezumkinВасилий Наумкин
15.08.2014 15:30

Очень просто.

В шаблоне:

[[+pagetitle:empty=`[[*pagetitle]]`]]

А в своём сниппете или плагине

$modx->setPlaceholder('pagetitle', 'Название страницы');
kondakovДмитрий Кондаков
15.08.2014 15:52

Понял, спасибо

inetlover
Александр Наумов
27.09.2022 22:16
Василий, спасибо тебе большое! Обалдеть, как круто!
Сергей Лелеко
26.09.2022 10:22
Тоже задействовал! Прекрасная функция!
inetlover
Александр Наумов
19.09.2022 11:56
Компонента Sidebar у Bootstrap нет, значит нужны только стили из Bootstrap-Vue @import &#39;~boot...
bezumkin
Василий Наумкин
13.08.2022 14:41
неужели, все глобальное, так просто можно взять и сделать Да, именно так. Мелкие затыки, конечно ж...
bezumkin
Василий Наумкин
13.08.2022 11:18
Killed говорит, что сервер прибивает задачу, скорее всего у провайдера есть какие-то лимиты по нагру...
bezumkin
Василий Наумкин
11.08.2022 18:46
Класс!! Рад, что тебе нравится! нужно добавить @click=&quot;showCart = true&quot; в: Спасибо, попра...
bezumkin
Василий Наумкин
09.08.2022 14:27
На здоровье!
inetlover
Александр Наумов
30.07.2022 22:58
Василий, спасибо большое! Первый вариант помог, не было вот этой настройки: php_value[sys_temp_dir].
bezumkin
Василий Наумкин
06.07.2022 17:06
Базовый Vue, базовый ES 6: импорт-экспорт модулей, вот это всё. Если что-то будет непонятно - просто...
bezumkin
Василий Наумкин
06.07.2022 14:19
Спасибо, что читаешь!