Всё, что доступно в .htm-файле: подключение частей,
вывод переменных, циклы и условия. Методы, которыми модуль передаёт сюда
данные, описаны в разделе «Методы шаблонизатора», преобразования
значений — в разделе «Модификаторы».
Итоговый HTML редко собирается из одного файла — обычно это шапка, футер, разные варианты контента, заглушки. Melbis даёт для этого два способа, и они не взаимоисключающие — их можно комбинировать в одном модуле.
TplParseКлассический способ уже описан выше, в разделе «Методы работы с шаблонами»: модуль явно решает, какой файл парсить и в какую переменную положить результат:
MELBIS()->TplParse($tpl, 'ORDER', 'order_absent');Такой подход оправдан, когда выбор шаблона зависит от бизнес-логики,
которая целиком живёт в PHP — например, модуль сам решает, парсить ли
order_absent или order_exist, на основании
данных, до которых у вёрстки нет доступа.
{^ИМЯ}Если же выбор частей — чисто вопрос вёрстки (когда показать
шапку/футер, когда — заглушку вместо блока), собирать структуру из PHP
не обязательно и даже нежелательно: это смешивает View-логику с
Controller-кодом. Для таких случаев подключить другой
.htm-файл того же модуля можно прямо в шаблоне, тегом
{^ИМЯ_ФАЙЛА}:
<div class="order-page">
{*ORDER}
{^ORDER_EXIST}
{ORDER*}
{*!ORDER}
{^ORDER_ABSENT}
{ORDER*}
</div>Модуль в этом случае ограничивается только передачей данных:
MELBIS()->TplAssign($tpl, 'ORDER', $order_data);а какую именно «ветку» вёрстки показать — решает сам шаблон
main.htm.
Как это работает:
.htm) в каталоге текущего модуля —
{^ORDER_ABSENT} подключит order_absent.htm из
той же папки units/{модуль}/, где лежит и вызывающий его
шаблон.{ROW},
{NUM1} и т. д.) этой конкретной итерации.Важное ограничение: нельзя разрывать один блок цикла
или условия между двумя разными подключениями — то есть нельзя вынести
открывающий {#STORE} в один файл, а закрывающий
{STORE#} в другой. Каждый подключаемый файл должен быть
самодостаточным фрагментом разметки.
Рекомендации:
Хотя технически движок сначала полностью разворачивает все
{^...} в единый текст и только потом ищет пары
{#KEY}...{KEY#} (поэтому разрыв цикла между двумя
подключаемыми файлами формально сработает), делать так не стоит — такая
связка не читается ни в одном из файлов по отдельности и легко ломается
при рефакторинге. Каждый подключаемый файл лучше держать самодостаточным
фрагментом разметки: либо содержит весь блок целиком, либо не содержит
открывающих/закрывающих тегов цикла или условия вовсе.
Способ вызов из шаблона {^ИМЯ} ближе к архитектуре
MVC, о которой шла речь в начале документа, и рекомендуется как способ
по умолчанию для разбивки разметки на переиспользуемые части.
Шаблонизатор работает по принципу безопасного конвейера: он тихо
игнорирует отсутствующие ключи (превращая null в пустоту),
блокирует прямой вывод массивов во избежание ошибок PHP и позволяет
выстраивать цепочки модификаторов.
Шаблонизатор Melbis Shop умеет работать не только с плоскими переменными, но и с глубоко вложенными массивами. Обмен данными между разными модулями системы описан в разделе «Глобальный массив».
Вывод значения:
{TITLE} — Стандартный вывод. Если ключ
TITLE присутствует в контексте, но его значение
null или пустая строка — парсер выведет пустую строку.
Если ключа нет в контексте вообще (переменная не
передана), тег останется в выводе как текст ({TITLE}) — это
сделано намеренно, чтобы опечатки в именах переменных были заметны в
готовом HTML, а не терялись молча.{{TITLE}} — Обязательный вывод. Ведёт
себя как {TITLE}, но если ключ отсутствует в контексте —
тег полностью вырезается (заменяется на пустую строку),
а не остаётся как текст. Удобно для необязательных полей
({{GIFT}}, {{DISCOUNT_BADGE}}), которые не
всегда приходят в данных, и которые не нужно каждый раз прятать через
{*GIFT}...{GIFT*}.URL-кодированный вывод:
[QUERY] — Применяет urlencode к значению
(пробел → +, кириллица → %D0%...). Для
формирования GET-параметров:
<a href="?search=[QUERY]">, и для аргументов
вложенного вызова модуля: {MELBIS:unit_name([ID])}.
Поведение при отсутствующем ключе — как у {TITLE} (тег
остаётся видимым как текст).[[QUERY]] — То же самое, но с поведением
{{TITLE}}: если ключа нет — тег вырезается полностью, а не
остаётся текстом.Пробел кодируется как + — по правилам
application/x-www-form-urlencoded. В query-string это
корректно, PHP разберёт значение обратно сам; в аргументах вызова модуля
— тоже, там парсер применяет парный urldecode. А вот в
сегменте пути плюс так и останется плюсом:
/search/[QUERY]/ для значения две звезды даст
/search/две+звезды/, и это уже не тот адрес. Для пути нужен
rawurlencode — он доступен как обычный модификатор (см.
«Модификаторы»):
<a href="/search/{QUERY|rawurlencode}/">Системные теги. Тринадцать значений —
{PATH}, {LANG}, {CSRF_TOKEN},
{BUILD} и другие — доступны в любом шаблоне любого модуля
без всякого TplAssign. Особняком стоит служебный
{NULL}. Все они, вместе с важной оговоркой про
модификаторы, собраны в разделе «Системные константы».
<link rel="stylesheet" href="{PATH}/statics/bundle.base.css">
<html lang="{LANG}">Если модуль передает в шаблон не просто строку, а сложный многомерный
массив (например, массив с настройками профиля или ценами), вы можете
обращаться к любому уровню вложенности через двоеточие
:.
Шаблонизатор автоматически “сплющивает” массивы, создавая удобные
ключи, что обеспечивает мгновенную скорость рендеринга.
Синтаксис: {МАССИВ:КЛЮЧ:ПОДКЛЮЧ} или
{МАССИВ:ИНДЕКС:КЛЮЧ}
Примеры применения:
{USER:PROFILE:PHONE} — выведет телефон из массива
['USER']['PROFILE']['PHONE'].{IMAGES:0:FILE_NAME} — Доступ по
индексу Выведет имя файла первой картинки из нумерованного
массива ['IMAGES'][0]['FILE_NAME'].{PRODUCT:PRICES:WHOLESALE|num:2} — Модификаторы
работают везде Подробнее — в разделе «Модификаторы». Вы можете
применять XSS-защиту, обрезку или форматирование чисел прямо к элементам
глубоких массивов.{USER:SETTINGS:AVATAR|def:/img/no-avatar.png} —
Установка значения по умолчанию, если вложенного ключа не
существует.Часто модули передают не сложные ассоциативные массивы, а обычные
списки значений (например, массив тегов, список ID, перечень цветов:
['red', 'blue', 'green']).
Для вывода таких списков в цикле парсер автоматически создает
виртуальный ключ ITEM, в который
помещается текущее значение.
Как это работает: Внутри цикла
{#СПИСОК} ... {СПИСОК#} вы просто используете тег
{ITEM}. Поскольку теперь это полноценный элемент движка, к
нему применимы все модификаторы и системные
переменные.
Пример (Вывод тегов через запятую, без запятой в конце):
Передан массив: $mData['TAGS'] = ['php', 'mysql', 'js'];
В шаблоне:
{#TAGS}
<a href="/search/?q=[ITEM]">{ITEM|html}</a>{*!IS_LAST}, {IS_LAST*}
{TAGS#}Используются для перебора массивов (например, списков товаров, свойств, файлов).
Синтаксис вызова и лимиты:
{#STORE} … {STORE#} — Выведет весь массив
целиком.{#STORE=4} … {STORE#} —
Лимит. Выведет только первые 4 элемента.{#STORE=2:4} … {STORE#} — Сдвиг и
Лимит. Пропустит первые 2 элемента и выведет следующие 4Пример:
{#STORE=4}
<div class="item {*IS_FIRST}first-item{IS_FIRST*}">
<b>{NUM1}. {NAME}</b>
</div>
{STORE#}Служебные переменные внутри цикла:
{NUM0} / {NUM1} — Индекс элемента (от 0) /
Порядковый номер (от 1).{COUNT} — Количество выводимых элементов в текущем
срезе (с учетом лимита).{IS_FIRST} / {IS_LAST} — Вернет
1, если это первый/последний элемент (удобно для добавления
классов).{IS_EVEN} / {IS_ODD} — Вернет
1 для четных/нечетных строк.{ROW} — Специальная переменная, содержащая
чистый оригинальный массив текущего элемента.
Используется для передачи всех данных строки в функции-модификаторы
(например, {ROW|json} или
{ROW|calc:...}).💡 Виртуальная переменная :COUNT (Проверки вне
цикла) При передаче любого индексного массива в шаблон, парсер
автоматически создает “плоскую” переменную с суффиксом
:COUNT, содержащую размер этого массива. Это позволяет
проверять количество элементов и строить логику еще до вызова
самого цикла:
{*STORE:COUNT>4}
<div class="alert">У нас более 4 товаров! Всего: {STORE:COUNT} шт.</div>
{STORE:COUNT>4*}Теги условий начинаются с * и закрываются так же.
Базовые проверки:
{*NAME} Текст {NAME*} — Выведется, если переменная не
пустая (не null, не '', не
0).{*!NAME} Текст {NAME*} — Отрицание (выведется, если
пусто).Операции сравнения:
{*PRICE>1000} Дорого {PRICE*} — Поддерживает
==, !=, <, >,
<=, >=.{*PRICE<OLD_PRICE} Скидка! {PRICE*}.Умные проверки (IN и BETWEEN):
{*STATUS==new,active} В работе {STATUS*} —
Проверка по списку. Сравнивает без учета регистра.
Сработает, если STATUS равен NEW, New или
active. Работает с == и !=.{*ID==1-10} Топ 10 {ID*} — Проверка
диапазона. Сработает, если ID от 1 до 10 включительно. Работает
с == и !=.Условия по системным тегам:
{*TEMPLATE==mobi} … {TEMPLATE*} — блок только для
группы шаблонов mobi, {*LANG==ru,uk} … {LANG*}
— для нужных языков. Системные теги ({TEMPLATE},
{LANG} и прочие — см. «Системные константы») участвуют в
условиях как обычные переменные, со всеми операциями сравнения. Это
основной способ обслужить одним файлом несколько групп или языков —
подробнее в «Группы шаблонов → Общий код между группами». Внутри такого
блока доступны системные и глобальные значения, но не локальные
переменные модуля.Почему нет составных условий (&&, ||)
Иногда хочется написать что-то вроде
{*NAME&&STATUS} ... {NAME*} — сразу в шаблоне. Это
сделано намеренно: составные условия не поддерживаются, и вот
почему.
Производительность. Каждое условие в шаблоне уже
проверяется отдельным проходом preg_match по всему тексту.
Логические связки &&/|| потребовали бы
либо писать мини-парсер выражений внутри тега, либо каскад вложенных
проверок — оба варианта ощутимо утяжеляют и замедляют разбор шаблона,
причём на каждой странице и на каждый рендер, а не один раз.
Верстка перестаёт быть верстальщику понятна.
{*NAME&&STATUS==1||PRICE>0} — это уже не
разметка, а код, который нужно читать как код: с приоритетом операторов,
скобками, отрицаниями. Верстальщик, открыв .htm-файл,
должен понимать что показывается, а не почему — а
сложное логическое выражение прямо в HTML заставляет его каждый раз
заново восстанавливать бизнес-смысл условия.
Условие превращается в недокументированный источник
истины. Если логика “товар считается отсутствующим, если нет
цены ИЛИ нет остатка ИЛИ статус снят с продажи” размазана по нескольким
местам вёрстки как {*PRICE==0||STOCK==0||STATUS==disabled},
то при изменении бизнес-правила (например, добавили ещё один статус)
придётся искать и синхронно править все места в шаблонах, где эта логика
продублирована. А опечатка в одном из них не будет заметна — просто в
одном месте условие сработает чуть иначе, чем в другом.
Тестируемость. Логику на PHP можно покрыть
unit-тестом. Логику, размазанную по .htm-файлам вперемешку
с версткой — практически нет.
Правильный способ — считать составное условие один раз в модуле и передать в шаблон уже готовый, самодостаточный по смыслу флаг:
$goods_absent = empty($goods['PRICE']) || empty($goods['STOCK']) || $goods['STATUS'] === 'disabled';
MELBIS()->TplAssign($tpl, 'GOODS_ABSENT', $goods_absent);{*GOODS_ABSENT}
<div class="out-of-stock">Товара нет в наличии</div>
{GOODS_ABSENT*}Теперь в вёрстке видно что проверяется
(GOODS_ABSENT — говорящее имя), а не набор операторов,
который нужно расшифровывать. Вся логика лежит в одном месте — в PHP,
рядом с остальной бизнес-логикой модуля, — и её можно переиспользовать,
протестировать и поменять, не трогая ни один .htm-файл.
Используем цепочки модификаторов для безопасной генерации микроразметки и мета-тегов.
<title>{PRODUCT_NAME|html} купить за {PRICE|nums} грн</title>
<meta name="description" content="{DESCRIPTION|plain|short:160}">
<script type="application/ld+json">
{ROW|json}
</script>Проверяем наличие, считаем процент скидки на лету и выводим заглушки.
<div class="product-card {*IS_FIRST}first-item{IS_FIRST*}">
<img src="{IMAGE_URL|def:/images/no-photo.png}" alt="{PRODUCT_NAME|html}">
<h3>{PRODUCT_NAME}</h3>
{*PRICE<OLD_PRICE}
<div class="badge-discount">
Скидка {ROW|calc:100-(price/old_price*100)|nums:0} %
</div>
<span class="old-price">{OLD_PRICE|num} грн</span>
{PRICE*}
<span class="current-price">{PRICE|num} грн</span>
{*STOCK_STATUS==instock,preorder}
<button onclick="addToCart({ID|int})">Купить</button>
{STOCK_STATUS*}
{*STOCK_STATUS!=instock,preorder}
<span class="out-of-stock">Нет в наличии</span>
{STOCK_STATUS*}
</div>Использование диапазонов (Range), дат и счетчиков циклов.
<table>
<tr>
<th>№</th>
<th>Дата</th>
<th>Сумма</th>
<th>Статус</th>
</tr>
{#ORDERS}
<tr class="{*IS_EVEN}bg-gray{IS_EVEN*}">
<td>{NUM1}</td>
<td>{CREATED_AT|date:d M Y}</td>
<td>{TOTAL|num:2,., } $</td>
<td>
{*STATUS_ID==1-3} <span class="badge-blue">{STATUS_NAME}</span> {STATUS_ID*}
{*STATUS_ID==4-5} <span class="badge-green">{STATUS_NAME}</span> {STATUS_ID*}
</td>
</tr>
{ORDERS#}
</table>