Секции ресурса (PRO)
Свои поля на обычной форме ресурса MODX — замена куче TV.
Вы создаёте секцию, кладёте в неё поля, и секция появляется на форме каждого ресурса. PageBlocks → Конструктор → Объекты, с контекстом ресурса.
Секция
| Настройка | Что делает |
|---|---|
| Название | Заголовок вкладки или панели |
| Контекст | pbResource — секция относится к форме ресурса |
| Размещение | tab, panel или default — как она появляется на форме |
| Описание | Текст над полями |
| Позиция | Порядок среди секций |
| Права | Какие пользователи и группы её видят |
| Опубликована | Неопубликованная секция не рисуется |
Размещение
| Значение | Куда попадают поля |
|---|---|
tab | Отдельная вкладка на форме ресурса |
panel | Сворачиваемая панель |
default | Среди стандартных полей, без контейнера |
tab — безопасный вариант для всего, где больше двух полей. default — для одного-двух полей, которым место рядом с родными полями ресурса: например, переключателя, меняющего поведение страницы.
Где лежат значения
В site_content.properties, в виде JSON. Не в новой колонке и не в таблице TV.
Это важно в две стороны:
- добавление поля не требует миграции, а удаление оставляет данные на месте, а не роняет колонку;
- значение путешествует вместе с ресурсом — дублирование ресурса копирует и его, потому что
propertiesMODX копирует сам.
Обычная выборка properties не разворачивает
Resource::find($id)->seo_title вернёт null, даже когда значение есть. Поле лежит внутри JSON, и разворачивает его только форма ресурса.
В коде читайте явно:
$page = Resource::find($id);
$title = $page->properties['seo_title'] ?? '';В шаблоне текущей страницы поля уже развёрнуты — их разворачивает контроллер.
Таблица и галерея — исключение
tablefield и galleryfield в properties не попадают. Это не недоделка, а разница в природе значения: у текстового поля значение одно — строка; у таблицы их столько, сколько строк завёл редактор, и у каждой свой набор полей; у галереи — сколько файлов загрузили. Такое в JSON ресурса не кладут, иначе набор нельзя ни отсортировать, ни показать постранично, ни отдать через API.
Поэтому оба типа держат свои записи в собственных таблицах, а к ресурсу привязываются тремя колонками:
| Колонка | Что в ней |
|---|---|
model_type | class_key ресурса, у обычной страницы — MODX\Revolution\modDocument |
model_id | id ресурса |
field_id | id самого поля в конструкторе |
| Тип поля | Куда пишутся записи |
|---|---|
tablefield | таблица модели, выбранной у таблицы; по умолчанию pb_table_data |
galleryfield | pb_files |
Сохранение секции оба типа пропускает намеренно: их записи уже в базе. Грид и загрузчик пишут их своими запросами по мере работы, и нажатие «Сохранить» им не нужно — в отличие от остальных полей, которые попадают в ресурс именно в этот момент.
Выбирайте по field_id, а не по морфу
field_id указывает на одно-единственное поле одной секции, поэтому пары model_id + field_id достаточно: строки соседнего поля той же страницы под выборку не попадут. А model_type в базе встречается и коротким именем (modDocument), и полным (MODX\Revolution\modDocument) — условие по нему молча теряет половину строк.
У нового ресурса привязывать не к чему
Пока ресурс не сохранён, у него нет id, и секции на форме не рисуются вовсе. Сначала создайте страницу и сохраните, потом заполняйте таблицу и галерею.
Таблица на форме ресурса
Зачем
Есть страницы, содержимое которых — не текст, а список однотипных записей: слайды промо-блока, характеристики товара, программа тура по дням, тарифы, вопросы и ответы, состав команды, приложенные документы.
В MODX такое обычно делают одним из двух способов, и оба неудобны. Либо TV с разделителями — редактор пишет Москва||Тверь||Клин, шаблон разбирает строку, а добавить к пункту картинку уже нельзя. Либо отдельный ресурс на каждый пункт — тогда в дереве сайта заводится сотня страниц, которые никто не открывает.
Поле-таблица даёт то, чего нет ни у того, ни у другого: у каждой строки свои поля своих типов (число, дата, картинка, richtext, селект), строки переставляются перетаскиванием, их можно публиковать и снимать с публикации по одной, а на фронте — сортировать, фильтровать и листать.
Как завести
- PageBlocks → Конструктор → Таблицы — создайте таблицу и опишите её поля. Это шаблон строки:
title,text,image,price. - PageBlocks → Конструктор → Объекты — создайте секцию с контекстом
pbResourceи размещениемtab. - В секции добавьте поле типа Таблица, укажите у него имя и выберите таблицу из пункта 1.
Настройки поля
| Настройка | Что делает |
|---|---|
| Таблица | Какой конструктор таблицы показывать |
| Только просмотр | Строки показываются списком: без кнопки создания, меню строки и редактирования |
| Поле для сортировки | По какой колонке упорядочить, по умолчанию menuindex |
| Направление | По возрастанию или по убыванию |
| Записей на странице | По умолчанию 20 |
Ширина принудительно выставляется во всю строку и не меняется — грид в узкой колонке бесполезен.
Пример: программа тура
Таблица TourDay с полями day (число), title (текст), text (richtext), image (картинка). Секция «Программа», контекст pbResource, размещение tab. В ней одно поле: подпись «Дни», имя days, тип «Таблица», таблица — TourDay.
Редактор открывает страницу тура, переходит на вкладку «Программа» и заводит дни как строки грида. На фронте:
[[!pbList?
&model=`PbTableData`
&parent=`[[*id]]`
&where=`[["field_id","=","12"]]`
&tpl=`chunk:tourDay`
&orderBy=`menuindex`
&limit=`30`
]]Чанк tourDay получает строку в переменной item:
<div class="tour-day">
<h3>День {$item->day}. {$item->title}</h3>
{if $item->image}<img src="/{$item->image}" alt="{$item->title|escape}">{/if}
{$item->text}
</div>12 — это id поля: он виден в колонке id грида полей конструктора. model=PbTableData годится, пока таблица пользуется моделью по умолчанию; если под неё сделана своя таблица через UI-миграции, подставьте имя её модели.
Остальные параметры — в pbList: &limit, &loadmore, &scopes, свои условия в &where.
Чего ждать не стоит
{$modx->resource->days} вернёт пусто
У ресурса нет свойства с именем поля-таблицы: строки лежат в своей таблице, и разворачивать их у ресурса некому. Пустота при этом не отличается от опечатки в имени — ошибки не будет. Читайте запросом, как в примере выше. То же касается и галереи.
Галерея на форме ресурса
Зачем
Одно поле-картинка закрывает обложку. Всё, что больше — фотоотчёт объекта, галерея работ, сертификаты, комплект документов к странице, — это набор файлов с порядком и подписями. Собирать его из десяти полей image_1 … image_10 бессмысленно: порядок не переставить, лишние поля висят пустыми, одиннадцатая фотография требует миграции.
Галерея даёт один загрузчик на всю страницу: файлы кидаются пачкой, порядок задаётся перетаскиванием, у каждого файла своя подпись и описание. Файлы — любые, не только картинки: видео и PDF лежат рядом с фотографиями, различает их колонка type.
Настройки поля
| Настройка | Что делает |
|---|---|
| Источник | Источник файлов MODX, в который грузим |
| Путь к файлам | Папка внутри источника |
| Миниатюры | Какие размеры генерировать, описываются JSON-ом |
Путь понимает плейсхолдеры {resource_id}, {user_id}, {alias} и {id}. На форме ресурса самый полезный — первый:
/gallery/{resource_id}/Файлы каждой страницы попадают в свою папку, и на диске видно, что чьё. Без плейсхолдера все галереи сайта складываются в один каталог, и через год там десять тысяч файлов.
Миниатюры описываются так:
{"webp": {"w": 1200, "h": 800, "q": 85, "zc": "1", "f": "webp"}}Получатся картинки 1200×800 в WebP, в папке webp рядом с оригиналами.
Пример: фотоотчёт объекта
Секция «Фотографии», контекст pbResource, размещение tab. Поле: подпись «Фотоотчёт», имя photos, тип «Галерея», источник — основной, путь /gallery/{resource_id}/.
Файлы галереи сниппетом не выбрать: pbList ходит по моделям PageBlocks\App\Models\, а PbFile — модель фреймворка. Поэтому запрос пишется сам — в контроллере или сниппете, а в шаблон уходит готовый список:
use Boshnik\PageBlocks\Models\PbFile;
$photos = PbFile::query()
->where('model_id', $modx->resource->id)
->where('field_id', 18)
->published()
->orderBy('menuindex')
->get();<div class="gallery">
{foreach $photos as $photo}
{if $photo->type === 'image'}
<img src="/{$photo->url}" alt="{$photo->title ?: $photo->name}">
{/if}
{/foreach}
</div>18 — снова id поля из грида конструктора. Полезные колонки строки: url, title, description, type, width, height, menuindex. url хранится без ведущего слэша — отсюда /{$photo->url} в примере.
Загруженный файл публикуется сразу, поэтому скоуп published() отсекает только то, что редактор снял с публикации руками.
Ветвитесь по type
В галерее лежат любые файлы, не только картинки: видео и PDF уживаются рядом с фотографиями. Без проверки type в вёрстку поедет <img> с PDF внутри.
Копирование и удаление ресурса
Дублирование ресурса копирует и то и другое. MODX сам копирует properties, то есть обычные поля секции, а PageBlocks на OnResourceDuplicate повторяет строки таблиц и записи галерей с новым model_id. Копируются записи, а не файлы на диске: новая строка галереи указывает на тот же файл.
Удаление ресурса записи не трогает. Удаление в MODX мягкое, страницу можно восстановить из корзины — вместе с таблицами и галереей. Но после очистки корзины строки остаются в своих таблицах привязанными к несуществующему id: на сайте их не видно, место они занимают. Чистить — вручную, по model_id.
Сохранение
Значения собираются на OnBeforeDocFormSave и записываются в ресурс. От вас ничего не требуется — достаточно секции.
Про то же самое для пользователей — Поля пользователя: механизм тот же, отличается контекст.