К содержанию

MODX Revolution · практика

Часто используемый код на Fenom в MODX

24 практических примера Fenom в MODX: поля ресурса, чанки, pdoResources, пагинация, MIGX, изображения и расписание.

Fenom на практике: как вывести карточки с TV, разбить список на страницы, разобрать MIGX и подготовить изображения. Разбираем 24 примера для повседневной работы с шаблонами.

При работе с MODX одни и те же задачи повторяются: собрать каталог, добавить обложку к статье, показать вопросы и ответы или найти ближайший старт курса. Обычно для этого уже есть всё необходимое — ресурсы, TV, чанки и сниппеты. В шаблоне остаётся связать данные с разметкой. Ниже разберём, как это сделать на Fenom.

Примеры рассчитаны на тех, кто уже работает с шаблонами MODX. Имена TV и чанков в примерах можно заменить своими. Перед использованием создайте нужные поля и чанки, привяжите TV к шаблонам и проверьте дополнения. ID ресурсов возьмите из своего сайта: числа из чужого проекта здесь не помогут.

Где работает Fenom и откуда приходят данные

Fenom — шаблонизатор, а не отдельная CMS. В рассматриваемых проектах он используется вместе с pdoTools. Синтаксис {$_modx->resource.pagetitle} работает там, где содержимое проходит через Fenom. Если фигурные скобки выводятся обычным текстом, проверьте парсер и настройки.

У pdoTools есть настройки обработки чанков и шаблонов: pdotools_fenom_default и pdotools_fenom_parser. Их поведение зависит от установленной версии; сверяйте его с документацией парсера. Не включайте доступ к произвольным PHP-функциям только ради примера: подготовку данных можно вынести в сниппет.

Частая причина одинаковых заголовков у всех карточек — обращение к открытой странице вместо строки выборки. $_modx->resource содержит данные текущей страницы; $pagetitle и $id внутри карточки — данные выбранного ресурса; $item.title в цикле — поле очередного элемента массива. Выбирайте источник по тому, что сейчас выводите.

Поля, условия и запасные значения

1. Заголовок с запасным значением

Для заголовка часто нужен простой запасной вариант: если редактор заполнил longtitle, берём его, иначе используем pagetitle. Запишем это условие:

{set $title = $_modx->resource.longtitle ?: $_modx->resource.pagetitle}
<h1>{$title | escape}</h1>

Оператор ?: выбирает запасное значение, если левое пусто. Он удобен для текста, но не для чисел, где ноль — допустимое значение. У H1 и HTML-тега title могут быть разные редакционные правила: не обязательно связывать их одним полем.

2. Условие вокруг необязательного блока

Если подзаголовок не заполнен, вместе с ним лучше убрать и контейнер. Иначе текст исчезнет, а отступ или рамка останутся. Проверим поле до открытия контейнера:

{if $_modx->resource.subtitle?}
  <div class="wm-intro">
    <p>{$_modx->resource.subtitle | escape}</p>
  </div>
{/if}

При пустом поле не останется рамки или лишнего отступа. Проверка ? отвечает на вопрос о непустом значении. Она не проверяет существование файла, правильность URL или допустимость введённого текста.

3. Задана ли переменная

Для числовых значений бывает важно сохранить ноль. Проверка существования отличается от проверки непустоты:

{if $price is set}
  <p>Стоимость: {$price | escape} ₽</p>
{/if}

is set проверяет, задана ли переменная. При этом она может содержать пустую строку: корректность цены проверяется при подготовке данных. О различии условий подробнее рассказано в статье о проверке переменных.

4. Текст и готовая разметка

Обычный текст и содержимое визуального редактора нельзя выводить одинаково. Для текстового описания используем экранирование, а редакторский HTML выводим как разметку:

<p>{$_modx->resource.description | escape}</p>
<div class="wm-text">{$_modx->resource.text1}</div>

Описание — обычный текст, поэтому экранируется. TV text1 здесь хранит контролируемый редакторский HTML: экранирование превратило бы теги в видимые символы. Пользовательский ввод сначала очищают на сервере. text1 нужно создать в вашем проекте — это не стандартное поле MODX.

Наследование шаблонов и параметры чанков

5. Общий каркас и дочерний шаблон

Создайте базовый шаблон wm-base: в нём будут общие шапка, подвал и место для содержимого страницы.

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <title>{$_modx->resource.pagetitle | escape}</title>
</head>
<body>
  {include 'wm-header'}
  <main>{block 'main'}{/block}</main>
  {include 'wm-footer'}
</body>
</html>

Создайте чанки wm-header и wm-footer. Это минимальный каркас: стили, скрипты и полные метаданные подключаются в основе вашего сайта. Дочерний шаблон заменяет содержимое именованного блока:

{extends 'template:wm-base'}
{block 'main'}
  <h1>{$_modx->resource.pagetitle | escape}</h1>
  <div class="wm-text">{$_modx->resource.content}</div>
{/block}

Префикс template: сообщает, что родитель — шаблон MODX. Имена блока в основе и потомке должны совпадать. Общую шапку меняют в одном месте, не обходя каждый шаблон страницы.

6. Передать параметры в чанк

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

{include 'wm-card'
  title=$_modx->resource.pagetitle
  description=$_modx->resource.description
  link=$_modx->makeUrl($_modx->resource.id)
}

Чанк wm-card работает с параметрами, не привязываясь к конкретной странице:

<article class="wm-card">
  <h3><a href="{$link | escape}">{$title | escape}</a></h3>
  {if $description?}<p>{$description | escape}</p>{/if}
</article>

Обратите внимание на имена переменных: здесь мы сами задаём параметры чанка, а в карточку pdoResources приходят поля строки выборки. Поэтому один вызов нельзя заменить другим без проверки чанка.

Списки ресурсов и карточки с TV

7. Дочерние страницы через pdoResources

Начнём с небольшого списка: выведем дочерние страницы текущего раздела. Для этого вызовем pdoResources:

{'pdoResources' | snippet : [
  'parents' => $_modx->resource.id,
  'depth' => 0,
  'tpl' => 'wm-resource-card',
  'limit' => 12,
  'sortby' => 'menuindex',
  'sortdir' => 'ASC'
]}

parents задаёт раздел, depth — глубину обхода, tpl — чанк одной карточки. limit здесь ограничивает весь блок двенадцатью записями. Это ещё не постраничный каталог: для большого раздела нужна пагинация. Чанк карточки создадим в следующем примере.

8. Подключить TV и согласовать имена

Для обложки и подзаголовка добавьте TV к выборке. Эти поля должны быть привязаны к шаблонам дочерних ресурсов:

{'pdoResources' | snippet : [
  'parents' => $_modx->resource.id,
  'depth' => 0,
  'tpl' => 'wm-resource-card',
  'includeTVs' => 'preview_img,subtitle',
  'prepareTVs' => 1,
  'tvPrefix' => '',
  'limit' => 12,
  'sortby' => 'menuindex',
  'sortdir' => 'ASC'
]}

Создайте чанк wm-resource-card. В нём используются данные строки, а не открытого раздела:

<article class="wm-card">
  {if $preview_img?}
    <img src="{$preview_img | escape}" alt="" loading="lazy">
  {/if}
  {set $title = $menutitle ?: $pagetitle}
  <h3><a href="{$_modx->makeUrl($id) | escape}">{$title | escape}</a></h3>
  {if $subtitle?}<p>{$subtitle | escape}</p>{/if}
</article>

includeTVs добавляет поля в выборку. prepareTVs подготавливает значения с учётом типа TV; это не универсальный декодер MIGX. processTVs запускает обработку вывода TV и требуется только когда она нужна. Пустой tvPrefix даёт переменную $preview_img. Если префикс tv. сохранён, используется $_pls['tv.preview_img']. Согласуйте вызов с чанком. Контракт параметров описан в документации pdoResources.

9. Получить массив вместо HTML

Иногда одного чанка карточки мало: например, первый элемент нужно оформить иначе или между группами вставить заголовки. Получим ресурсы массивом и переберём их в шаблоне:

{set $items = 'pdoResources' | snippet : [
  'parents' => $_modx->resource.id,
  'depth' => 0,
  'return' => 'data',
  'limit' => 12,
  'sortby' => 'menuindex',
  'sortdir' => 'ASC'
]}
{if $items is array}
  <ul>
  {foreach $items as $item}
    <li><a href="{$_modx->makeUrl($item.id) | escape}">
      {$item.pagetitle | escape}
    </a></li>
  {/foreach}
  </ul>
{/if}

return => 'data' возвращает массив вместо готовой разметки. Его можно перебрать, но нельзя просто вывести как HTML. Для одинаковых карточек обычно достаточно tpl. Если записей нет и пустой ul не нужен, проверяйте количество элементов до открытия списка.

Пагинация и связанные материалы

10. Список с pdoPage

Выведем статьи по 16 записей на страницу:

{'!pdoPage' | snippet : [
  'element' => 'pdoResources',
  'parents' => $_modx->resource.id,
  'depth' => 0,
  'tpl' => 'wm-resource-card',
  'includeTVs' => 'preview_img,subtitle',
  'prepareTVs' => 1,
  'tvPrefix' => '',
  'limit' => 16,
  'pageVarKey' => 'page',
  'pageNavVar' => 'page.nav',
  'sortby' => 'publishedon',
  'sortdir' => 'DESC'
]}
{'page.nav' | placeholder}

pdoPage оборачивает выборку и создаёт навигацию. Плейсхолдер читается после вызова. Проверьте первую, вторую и последнюю страницы. Для двух независимых списков задайте разные pageVarKey и имена служебных плейсхолдеров. Их назначение смотрите в документации pdoPage.

Знак ! задаёт некэшируемый MODX-вызов — это важно для результата, зависящего от запроса. Внутренний кэш дополнения — отдельный механизм: знак не отменяет все уровни кэширования. Если список состоит из разных типов ресурсов, добавьте фильтр по вашему шаблону.

11. Похожие материалы без текущей страницы

В конце статьи можно показать другие материалы того же раздела. Текущую статью исключим из подборки:

{set $related = 'pdoResources' | snippet : [
  'parents' => $_modx->resource.parent,
  'depth' => 0,
  'where' => ['id:!=' => $_modx->resource.id],
  'tpl' => 'wm-resource-card',
  'limit' => 6,
  'sortby' => 'menuindex',
  'sortdir' => 'ASC'
]}
{if $related?}
  <section>
    <h2>Ещё в этом разделе</h2>
    {$related}
  </section>
{/if}

Весь блок исчезает, если соседних материалов нет. id:!= исключает текущий ресурс. Если в разделе мало материалов, можно отдельным условием расширить выборку до родительского раздела. Такой переход имеет смысл только при понятной структуре каталога.

12. URL ресурса вместо ручной сборки

Адрес ресурса можно получить через makeUrl или модификатор url. Метод строит адрес по ID:

<a href="{$_modx->makeUrl($_modx->resource.parent) | escape}">
  Вернуться в раздел
</a>
{set $absoluteUrl = $_modx->makeUrl($_modx->resource.id, '', '', 'full')}

Вариант full нужен для абсолютного адреса. На странице верхнего уровня проверьте, куда должна вести ссылка возврата: её родитель может быть равен нулю. Канонический URL фильтра или постраничного списка требует отдельной логики, а не только этого вызова.

13. Общие контакты в ресурсе настроек

Общие контакты можно хранить в отдельном ресурсе настроек. $settingsId — ID вашей страницы настроек, переданный из основы:

{set $phone = $settingsId | resource : 'phone1'}
{if $phone?}
  <p>Телефон: {$phone | escape}</p>
{/if}

Поле меняют один раз и используют в шапке, подвале и контактах. Не путайте его с системной настройкой: $_modx->config.site_url приходит из конфигурации, phone1 здесь — TV другого ресурса. Для tel: подготовьте нормализованный номер отдельно от оформленного текста.

14. Хлебные крошки из структуры

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

<nav aria-label="Хлебные крошки">
{'pdoCrumbs' | snippet : [
  'showHome' => 1,
  'showCurrent' => 1,
  'tplWrapper' => '@INLINE <ol>{$output}</ol>',
  'tplHome' => '@INLINE <li><a href="{$link | escape}">Главная</a></li>',
  'tpl' => '@INLINE <li><a href="{$link | escape}">{$pagetitle | escape}</a></li>',
  'tplCurrent' => '@INLINE <li aria-current="page">{$pagetitle | escape}</li>'
]}
</nav>

Последний пункт показывает текущую страницу без ссылки на саму себя. Если цепочка неверная, проверьте родителей ресурсов: крошки отражают структуру MODX. Длинные @INLINE-шаблоны удобнее вынести в именованные чанки.

MIGX: JSON, массив и рендер через сниппет

MIGX позволяет редактировать повторяемые строки: преимущества, вопросы, слайды, расписание. Для примера вопросов настройте TV faq с текстовыми полями question и answer. Его значение — JSON-массив:

[{"question":"Как заказать?","answer":"Оставьте заявку."}]

15. Разобрать JSON и вывести цикл

JSON можно разобрать прямо в шаблоне. Перед циклом проверим, что получили массив:

{set $faqs = json_decode($_modx->resource.faq, true)}
{if $faqs is array}
  {foreach $faqs as $faq}
    <details>
      <summary>{$faq.question | escape}</summary>
      <p>{$faq.answer | escape}</p>
    </details>
  {/foreach}
{/if}

Второй аргумент true возвращает ассоциативный массив. Пример предполагает доступность json_decode в вашей конфигурации Fenom. Если функция недоступна, декодируйте данные в сниппете. Некорректный JSON не должен попасть в цикл. Ответ здесь — простой текст; редакторский HTML требует другого контракта.

16. MIGX через getImageList

Установите MIGX через управление пакетами MODX. В его состав входит getImageList: несмотря на название, он выводит не только изображения, но и вопросы, преимущества и другие строки. Создайте TV faq с полями question и answer, привяжите его к шаблону и заполните. Вызов для текущей страницы:

{'getImageList' | snippet : [
  'tvname' => 'faq',
  'tpl' => 'wm-faq-item',
  'limit' => 0
]}

tvname — имя MIGX-поля, tpl — чанк одной строки. Чанк создадим ниже. Нулевой limit выводит все строки; для большого набора задайте ограничение. Чтобы получить строки другого ресурса, передайте его ID в docid.

17. Чанк строки для getImageList

Создайте чанк wm-faq-item. Для переносимого примера используем обычные плейсхолдеры MODX: их обработка не требует включения Fenom внутри чанка.

<details>
  <summary>[[+question:htmlent]]</summary>
  <p>[[+answer:htmlent]]</p>
</details>

Названия плейсхолдеров совпадают с полями MIGX. Фильтр htmlent экранирует текст для HTML. Здесь ответы содержат обычный текст, а не разметку визуального редактора. Сам вызов сниппета остаётся в Fenom-шаблоне, а чанк строки обрабатывается парсером MODX.

{'getImageList' | snippet : [
  'tvname' => 'faq',
  'tpl' => 'wm-faq-item',
  'limit' => 3
]}

В этом вызове выводятся первые три строки. Остальные остаются в TV и появятся при увеличении или снятии лимита.

18. Не добавлять базовый путь дважды

В MIGX может храниться только имя файла, без пути к медиатеке. В этом случае к имени добавляют базовый URL. При прямом разборе JSON это выглядит так:

{set $imageUrl = $imagesBaseUrl ~ $item.image}
<img src="{$imageUrl | escape}" alt="{$item.title | escape}" loading="lazy">

$imagesBaseUrl задаёт ваш шаблон: это URL базы, а не путь на диске. Фрагмент подходит, если $item.image — относительное имя без базы. Пользовательский сниппет может уже добавлять базу медиатеки или отдельно обрабатывать абсолютные адреса. Сначала проверьте результат, чтобы не получить assets/images/assets/images/photo.jpg.

Изображения и адаптивные размеры

19. Производное изображение через pThumb

Нужен установленный pThumb. Для обложки с пропорциями 4:3:

{if $_modx->resource.preview_img?}
  {set $src = $_modx->resource.preview_img | pthumb : 'w=800&h=600&zc=1&q=80'}
  <img src="{$src | escape}" alt=""
       width="800" height="600" loading="lazy">
{/if}

zc=1 разрешает обрезку. Для схем и произведений, у которых важны края, выберите сохранение кадра. Пустой alt подходит декоративной обложке рядом с заголовком; содержательное изображение требует описания. loading="lazy" используйте ниже первого экрана. Подробнее — в статье о TV-изображениях.

20. Две ширины через pThumb

Подготовим две версии изображения через установленный pThumb. Браузер выберет подходящую из srcset:

{if $image?}
  {set $small = $image | pthumb : 'w=640&q=80'}
  {set $large = $image | pthumb : 'w=1280&q=80'}
  <img src="{$small | escape}"
       srcset="{$small | escape} 640w, {$large | escape} 1280w"
       sizes="(max-width: 680px) 100vw, 640px"
       alt="{$title | escape}" loading="lazy">
{/if}

$image и $title передаются в чанк. sizes описывает ширину картинки в макете. Здесь исходное изображение должно быть не меньше 1280 пикселей по ширине: дескрипторы 640w и 1280w должны совпадать с фактическими ширинами файлов. Проверьте результат обработки и добавьте width и height по известным пропорциям.

Расписание и формы числительных

21. Ближайшая будущая строка расписания

Чтобы показать ближайший старт курса, перебираем расписание, пропускаем прошедшие даты и останавливаемся на первой будущей. В нашем примере дата хранится в числовом поле start_ts как Unix-время:

{set $next = []}
{foreach $schedule as $row}
  {if $row.start_ts < $now}
    {continue}
  {/if}
  {set $next = $row}
  {break}
{/foreach}
{if $next?}
  <p>Ближайший старт: {$next.start_label | escape}</p>
{else}
  <p>Дату следующего старта уточните у организатора.</p>
{/if}

Массив $schedule, Unix-время $now и строку start_label готовит сниппет. Это адаптация алгоритма, не вызов готового дополнения. Строки должны быть заранее отсортированы по дате, иначе первая будущая запись не обязательно ближайшая. Правила часового пояса и границы даты должны быть общими для всей системы записи.

22. Три формы русского числительного

Для правильной подписи количества нужны последние две цифры числа. Входное $count — целое неотрицательное количество:

{set $n100 = $count % 100}
{set $n10 = $count % 10}
{if $n10 == 1 && $n100 != 11}
  {set $word = 'работа'}
{elseif $n10 >= 2 && $n10 <= 4 && ($n100 < 10 || $n100 >= 20)}
  {set $word = 'работы'}
{else}
  {set $word = 'работ'}
{/if}
<p>{$count} {$word}</p>

Проверьте 0, 1, 2, 5, 11, 14, 21, 22 и 25. Не загружайте весь большой каталог только ради count. У пагинированной выборки используйте общий счётчик, у отдельной статистики — соответствующий запрос в сниппете.

23. Показать секцию после получения HTML

Сначала получите строки, затем решите, нужен ли весь блок. Это избавит страницу от пустого заголовка «Вопросы и ответы»:

{set $rows = 'getImageList' | snippet : [
  'tvname' => 'faq',
  'tpl' => 'wm-faq-item',
  'limit' => 0
]}
{if $rows?}
  <section aria-labelledby="faq-title">
    <h2 id="faq-title">Частые вопросы</h2>
    {$rows}
  </section>
{/if}

Само присваивание результата не показывает: важно вывести $rows внутри секции. Здесь строка уже содержит HTML чанка, поэтому её целиком не экранируют. Текстовые поля экранируются в чанке строки. При нескольких FAQ-блоках на странице используйте уникальные ID заголовков.

Как проверять код и что не стоит копировать

24. Проверить контракт данных

Если список не выводится, начните с результата сниппета: он вообще вернул массив? Для проверки добавьте во временный тестовый шаблон небольшой вызов:

{set $items = 'pdoResources' | snippet : [
  'parents' => $_modx->resource.id,
  'depth' => 0,
  'return' => 'data',
  'limit' => 3
]}
{if $items is array}
  <p>Записей в пробной выборке: {$items | count}</p>
{else}
  <p>Проверьте результат выборки и настройки парсера.</p>
{/if}

Лимит в три записи нужен только для проверки. Полученное количество не равно числу всех дочерних ресурсов. Этот фрагмент показывает тип результата, не выводя содержимое базы; после проверки уберите его из шаблона.

При переносе примера посмотрите, сколько данных он обработает. limit => 0 подходит для нескольких категорий, но большой каталог целиком займёт память. Запрос внутри foreach выполнится для каждой карточки. Для каждого дополнения проверьте параметры по документации установленной версии.

  1. Проверьте назначенный шаблон и обработку Fenom.
  2. Проверьте привязку TV, заполненность и ожидаемый формат.
  3. Разделите поля открытой страницы и строки карточки; согласуйте tvPrefix.
  4. Проверьте имена чанков, наличие сниппетов и параметры вашей версии.
  5. Проверьте пустые поля, отсутствие строк и количество выше лимита.
  6. Откройте ссылки и производные изображения: пути не должны дублировать базу медиатеки.
  7. Для пагинации проверьте вторую страницу, для расписания — границу даты, для числительных — 11–14.
  8. После правок проверьте кэш, журнал MODX, консоль и сетевые ошибки браузера.

Для первого применения возьмите один раздел своего сайта: подключите карточку с TV и выведите её через pdoResources. Проверьте заполненную карточку и карточку без обложки, а затем пустой раздел. Если записей больше, чем помещается на странице, добавьте pdoPage и перейдите на вторую страницу. Так вы проверите весь сценарий, прежде чем переносить его в остальные разделы.

Документация и дальнейшее чтение

API сверяется с документацией: парсер pdoTools, pdoResources, pdoPage, Fenom. Для вывода MIGX см. документацию getImageList. Основы структуры объяснены отдельно в статье о шаблонах, чанках и TV.

Когда нужен второй взгляд

Нужна помощь с вашим сайтом?

Если задача выходит за рамки статьи, расскажите, что хотите исправить или добавить.

Обсудить задачу