Вывод списков в шаблонах CoTemplate: два подхода, их различия и практическое применение
Различия и практическое применение в Cotonti способов обработки циклов на стороне шаблона в FOR {KEY}, {VAL} IN {SET} или в бизнес логике через foreach в PHP-контроллере.
Оглавление
- Введение
- Базовые понятия
- Устройство CoTemplate
- Подход первый:
<!-- FOR --> - Подход второй:
<!-- BEGIN -->+$t->parse() - Разбор на примере: список товаров
- Разбор на примере: дерево категорий
- Сравнительный анализ
- Руководство по выбору
- Типичные ошибки
- Словарь терминов
1. Введение
На любом сайте есть списки: товары в каталоге, новости на главной, разделы в меню. Задача разработчика — превратить набор однотипных данных в HTML, где разметка повторена для каждого элемента, а внутрь подставлены значения.
В Cotonti эта задача решается шаблонизатором CoTemplate (system/cotemplate.php). У него есть два инструмента для вывода списков, и они устроены по-разному.
Первый — конструкция <!-- FOR --> внутри шаблона. Цикл выполняет сам шаблонизатор (класс Cotpl_loop): он получает массив, обходит его, повторяя разметку. PHP-цикл не нужен.
Второй — блок <!-- BEGIN --> ... <!-- END --> в шаблоне и цикл foreach в PHP-контроллере. Шаблон описывает, как выглядит одна строка. PHP-цикл наполняет её данными через $t->assign, накапливает готовые строки вызовом $t->parse('MAIN.ROW'), а финальный $t->parse('MAIN') склеивает их в общий список (класс Cotpl_block).
Оба инструмента дают на выходе HTML. Но их поведение определяется исходным кодом Cotpl_loop и Cotpl_block — и именно эти различия определяют, какой подход применим в какой ситуации.
Кому полезна статья. Администраторам, редактирующим шаблоны; разработчикам модулей и плагинов; всем, кто столкнулся с ситуацией «список не выводится» и хочет понять причину.
Источники. Все утверждения опираются на исходный код system/cotemplate.php и официальное описание синтаксиса CoTemplate. Приводятся имена классов, методов, свойств и конкретные проверки.
2. Базовые понятия
Шаблон — файл .tpl, в котором HTML смешан со специальным синтаксисом: теги {TAG} и управляющие конструкции в комментариях (<!-- BEGIN -->, <!-- FOR -->, <!-- IF -->, {FILE ...}).
Тег {TAG} — место подстановки значения. Локальные теги читаются из $t->vars (массива, заполняемого через $t->assign). Глобальные теги вида {PHP.var} читаются из $GLOBALS.
Управляющая конструкция — указание шаблонизатору повторить фрагмент (FOR), показать при условии (IF), вставить содержимое файла (FILE).
Шаблонизатор — программа, читающая шаблон, выполняющая конструкции и возвращающая HTML. В Cotonti код — system/cotemplate.php. Классы: XTemplate (фасад), Cotpl_block (блок <!-- BEGIN -->), Cotpl_loop (цикл <!-- FOR -->), Cotpl_logical (<!-- IF -->), Cotpl_expr (выражения в IF), Cotpl_data (текст и теги), Cotpl_var (теги и их вычисление).
PHP-контроллер — код, который готовит данные, передаёт их в шаблон через $t->assign(), запускает рендеринг через $t->parse() и выводит через $t->out().
Простейший пример:
$t->assign('PAGE_TITLE', 'О компании');
$t->parse('MAIN');
$t->out('MAIN');
<!-- BEGIN: MAIN -->
<h1>{PAGE_TITLE}</h1>
<!-- END: MAIN -->
Пользователь увидит <h1>О компании</h1>. Когда данных несколько (три новости, пять товаров), одного тега недостаточно — нужен инструмент повторения. Их два.
3. Устройство CoTemplate
Поведение списков напрямую определяется тем, как устроены классы Cotpl_block и Cotpl_loop. Ниже — только то, что подтверждается исходным кодом.
3.1. XTemplate — фасад
Публичное свойство $vars — ассоциативный массив присвоенных локальных тегов. Защищённые $blocks (корневые блоки) и $index (карта «полное имя блока → путь»).
Публичные методы:
assign($name, $val = null, $prefix = '')— присваивает тег или массив тегов. При массиве ключи получают префикс$prefix. Регистр имени сохраняется:$t->assign('tag', ...)создаёт тегtag. Повторныйassignс тем же именем затирает значение.parse($block = 'MAIN')— находит блок по$index, спускается по пути и вызывает$blk->parse($this).text($block = 'MAIN')— находит блок и вызывает$blk->text($this).out($block = 'MAIN')— выводитtext()в браузер (или дамп в debug-режиме).reset($block = 'MAIN')— находит блок и вызывает$blk->reset().hasBlock($name),hasTag($name),getTags(),get($name)— вспомогательные.
3.2. Cotpl_block — блок <!-- BEGIN -->
Защищённое $data — массив накопленных рендеров. Публичное $blocks — вложенные блоки.
Три ключевых метода (по CoTemplate):
public function parse($tpl) {
$data = '';
foreach ($this->blocks as $block) {
$data .= $block->text($tpl);
}
$this->data[] = $data;
}
public function text($tpl) {
$text = implode('', $this->data);
$this->data = [];
return $text;
}
public function reset($path = []) {
$this->data = [];
}
Из этого кода видны три свойства:
parseнакапливает. Каждый вызов добавляет одну строку в$this->data.textсклеивает и очищает. После первого вызова$this->dataпуст. Повторныйtextбез новыхparseвернёт пустую строку.resetочищает накопленное без вывода.
3.3. Cotpl_loop — цикл <!-- FOR -->
Наследник Cotpl_block. Защищённые свойства: $key (имя тега для ключа итерации, опционально), $val (имя тега для текущего элемента), $set (объект Cotpl_var — источник).
Метод parse и reset переопределены так, что бросают исключение:
public function parse($xtpl) {
throw new Exception('Calling parse() on a loop');
}
public function reset($dummy = null) {
throw new Exception('Calling reset() on a loop');
}
Метод text (по CoTemplate):
public function text($tpl) {
$data = '';
$set = $this->set->evaluate($tpl);
if (is_array($set) && $this->blocks) {
foreach ($set as $key => $val) {
$tpl->assign($this->val, $val);
if (!empty($this->key)) {
$tpl->assign($this->key, $key);
}
foreach ($this->blocks as $block) {
$data .= $block->text($tpl);
}
}
}
return $data;
}
Из этого кода видны ключевые особенности:
- Только массив. Проверка
is_array($set)обязательна. Объект (дажеTraversable), скаляр,null— цикл не выполняется, возвращается пустая строка. $this->dataне используется. Результат копится в локальной переменной и возвращается сразу.parseиresetбросаютException. Наполнить FOR-блок из PHP через$t->parse('MAIN.FOR_BLOCK')нельзя. Сбросить через$t->reset('MAIN.FOR_BLOCK')нельзя.
3.4. Cotpl_var — теги и их вычисление
Конструктор разбирает строку тега: сначала |-фильтры, затем точки-ключи. {PRODUCT.NAME} → name = 'PRODUCT', keys = ['NAME'].
Метод evaluate($tpl = null):
- Для
name === 'PHP'берёт$GLOBALS. - Иначе читает
$tpl->vars[$this->name](если имя не задано — создаёт ключ со значениемnull). - При наличии
$this->keysспускается по ним: для объектов —$var->{$key}, для массивов —$var[$key]. Для последнего ключа проверяетisset. Если ключ не найден —$val = null. - При наличии
$this->callbacksпоследовательно применяет функции.
Критическая деталь: для массива проверка идёт через isset($var[$last_key]) с точным совпадением регистра. Тег {PRODUCT.NAME} ищет ключ NAME, а не name. Если в массиве ['name' => ...] — вернётся null, тег выведет пустоту.
Дополнительно: функция Cotpl_data::splitToChunks распознаёт тег только если первый символ после { входит в [A-Z0-9_$]. Тег {product.name} (со строчной первой буквой) не будет распознан и останется в выводе как текст.
3.5. Cotpl_logical — <!-- IF -->
Метод parse и reset также бросают Exception ('Calling parse() on logical block', 'Calling reset() on logical block'). Метод text вычисляет выражение Cotpl_expr и рендерит нужную ветку.
3.6. Cotpl_data — текст и теги
Разбивает фрагмент на куски (splitToChunks) и при рендеринге склеивает их. Для Cotpl_var вызывает evaluate; если результат — массив или объект, применяет var_export($tmp, true); иначе приводит к строке. Скаляры выводятся как есть.
3.7. Компиляция и кеш
XTemplate::restart($path) читает файл, обрабатывает {FILE ...}, вычисляет хеш, и если кеш отключён или устарел — вызывает compile($code), который извлекает корневые блоки регуляркой <!--\s*BEGIN:\s*([\w_]+)\s*-->(.*?)<!--\s*END:\s*\1\s*-->. Вложенные BEGIN, FOR, IF разбираются в Cotpl_block::compile.
Скомпилированное дерево объектов может кешироваться на диск (self::$cacheEnabled, self::$cacheDir . '/templates/'). Имена файлов кеша включают хеш содержимого.
3.8. Единое пространство тегов
Согласно официальному описанию синтаксиса: «Имя ключа и значение находятся, как и другие теги, в едином с ними пространстве. В шаблонизаторе нет понятия области видимости или стеков». Cotpl_loop::text присваивает VAL и KEY через $tpl->assign, то есть пишет в общий $t->vars. После завершения цикла эти теги сохраняют значения последней итерации.
4. Подход первый: цикл внутри шаблона через <!-- FOR -->
4.1. Идея
Контроллер передаёт массив одним вызовом: $t->assign('LIST', $products). В шаблоне описывается цикл <!-- FOR --> ... <!-- ENDFOR --> с разметкой одной итерации. При рендеринге Cotpl_loop::text обходит массив, присваивает текущий элемент и повторяет разметку.
4.2. Синтаксис
Согласно регуляркам в Cotpl_loop::__construct, две формы:
<!-- FOR {VAL} IN {SET} -->
<!-- FOR {KEY}, {VAL} IN {SET} -->
{VAL}— имя тега для текущего элемента (\w+).{KEY}— необязательное имя тега для ключа итерации.{SET}— источник ([\w\.\-]+с опциональным|...).
Закрывается <!-- ENDFOR -->.
4.3. Пример
<!-- BEGIN: MAIN -->
<ul>
<!-- FOR {PRODUCT} IN {PRODUCTS_LIST} -->
<li>
{PRODUCT.NAME|htmlspecialchars} — {PRODUCT.PRICE} ₽
</li>
<!-- ENDFOR -->
</ul>
<!-- END: MAIN -->
4.4. Что можно внутри
{VAL}— текущий элемент. Если массив или объект — выводится черезvar_export; если скаляр — как строка.{VAL.KEY}— спуск по ключам (для массива —$current['KEY'], для объекта —$current->KEY).{KEY}— текущий ключ массива (если объявлен).{VAL.KEY|функция}— фильтр.- Любые локальные и глобальные теги.
- Вложенные
<!-- IF -->, вложенные<!-- FOR -->. {PHP|range(1,10)}в качестве источника — подтверждено синтаксисом и кодом (name === 'PHP'→$GLOBALS, затем callbacks).
4.5. Ограничения по коду
Из Cotpl_loop::text:
- Только массив.
is_array($set)— обязательное условие.Traversable, генератор, скаляр — цикл не выполнится, вернётся пустая строка. - Регистр ключей массива важен. Тег
{PRODUCT.NAME}ищет$current['NAME']черезisset. Ключnameне найдётся. - Нет накопления.
$this->dataне используется; результат возвращается сразу.
Из Cotpl_loop::parse и Cotpl_loop::reset:
$t->parse('MAIN.FOR_BLOCK')бросаетException('Calling parse() on a loop').$t->reset('MAIN.FOR_BLOCK')бросаетException('Calling reset() on a loop').
Из поведения компиляции:
- Вложенный FOR работает для фиксированной глубины. Внутренний
<!-- FOR -->компилируется в отдельный объектCotpl_loopвнутри$this->blocksвнешнего. При рендеринге внешний цикл на каждой итерации вызываетtextвнутреннего. Это даёт корректную вложенность для статически описанной структуры. Однако динамическая глубина (дерево с произвольным числом уровней) так не собирается: каждый уровень должен быть явно прописан в шаблоне.
4.6. Повторный рендер (уточнение)
Важно различать parse и out/text.
$t->parse('MAIN')один раз:Cotpl_block::parseобходит дочерние блоки, вызываетCotpl_loop::text, результат попадает вMAIN.$data.$t->out('MAIN')(илиtext): вызываетCotpl_block::textдляMAIN, который склеиваетMAIN.$dataи очищает его.
Отсюда:
- Повторный
$t->out('MAIN')без новогоparse('MAIN')вернёт пустую строку —MAIN.$dataуже очищен.Cotpl_loop::textпри этом не вызывается. - Повторный
$t->parse('MAIN')+$t->out('MAIN')снова вызоветCotpl_loop::text(при обходе дочерних блоков) и выведет HTML ещё раз.
Утверждение «повторный out выведет тот же HTML» неверно: out не запускает цикл заново, он только читает накопленное MAIN.$data, которое после первого out пусто.
4.7. Побочный эффект тегов VAL/KEY
Поскольку Cotpl_loop::text пишет VAL и KEY в общий $t->vars, после завершения цикла эти теги сохраняют значения последней итерации. Использование {PRODUCT} вне цикла выведет последний товар, а не ошибку.
5. Подход второй: блок <!-- BEGIN --> с наполнением из PHP
5.1. Идея
В шаблоне описывается блок <!-- BEGIN: ROW --> ... <!-- END: ROW --> — образец одной строки. PHP-цикл foreach присваивает теги для текущей строки и вызывает $t->parse('MAIN.ROW'). Каждый вызов Cotpl_block::parse добавляет строку в $this->data. Финальный $t->parse('MAIN') вызывает Cotpl_block::text для ROW, склеивает накопленное и очищает. Повтор делает PHP; шаблон описывает один элемент.
5.2. Синтаксис
Шаблон:
<!-- BEGIN: BLOCK_NAME -->
...разметка...
<!-- END: BLOCK_NAME -->
PHP:
foreach ($items as $item) {
$t->assign([...]);
$t->parse('MAIN.BLOCK_NAME');
}
$t->parse('MAIN');
$t->out('MAIN');
5.3. Пример
<!-- BEGIN: MAIN -->
<ul>
<!-- BEGIN: SHOP_ROW -->
<li class="{SHOP_ROW_ODDEVEN}">
<a href="{SHOP_ROW_URL}">{SHOP_ROW_TITLE}</a>
— {SHOP_ROW_PRICE} ₽
</li>
<!-- END: SHOP_ROW -->
</ul>
<!-- END: MAIN -->
Внутри блока — плоские теги (SHOP_ROW_URL, SHOP_ROW_TITLE), присваиваемые напрямую в PHP.
5.4. Что можно внутри
- Плоские теги
{TAG_NAME}, присвоенные через$t->assign. - Теги с фильтрами
{TAG_NAME|htmlspecialchars}. - Вложенные
<!-- IF -->,<!-- BEGIN -->,<!-- FOR -->. - Теги, содержащие готовый HTML (например, результат ручной сборки вложенного списка).
5.5. Возможности по коду
Из Cotpl_block::parse:
- Накопление. Каждый
$t->parse('MAIN.ROW')добавляет строку в$this->data.
Из Cotpl_block::text:
- Склейка и очистка. После
$t->text('MAIN')накопленное обнуляется.
Из Cotpl_block::reset:
- Очистка без вывода.
$t->reset('MAIN.ROW')очищает накопленное.
Из общего поведения:
- Любой итерируемый источник. Цикл пишет PHP, поэтому можно перебирать массив, объект с
Traversable, генератор. - Регистр ключей исходного массива не важен. Тег задаётся целиком (
SHOP_ROW_URL); извлечение значения из массива выполняет PHP. - Рекурсивный
parseиз PHP технически возможен, но модель накопления даёт плоский список в порядке вызовов, а не вложенный HTML (см. §7).
5.6. Повторный рендер
- После
$t->out('MAIN')MAIN.$dataочищен. Повторный$t->out('MAIN')без новыхparse('MAIN.ROW')вернёт пустую строку. - Чтобы вывести список заново, нужно повторить цикл
assign+parse('MAIN.ROW')и финальныйparse('MAIN'). $t->reset('MAIN.ROW')очищает накопленное без вывода — полезно перед новым наполнением.
6. Разбор на примере: список товаров
6.1. Данные
$products = [
['NAME' => 'Ноутбук', 'PRICE' => 45000, 'CODE' => 'notebook'],
['NAME' => 'Мышь', 'PRICE' => 1200, 'CODE' => 'mouse'],
['NAME' => 'Клавиатура', 'PRICE' => 3500, 'CODE' => 'keyboard'],
];
6.2. Подход <!-- FOR -->
Контроллер:
$t->assign('SHOP_PRODUCTS', $products);
$t->parse('MAIN');
$t->out('MAIN');
Шаблон:
<!-- BEGIN: MAIN -->
<ul class="shop-list">
<!-- FOR {PRODUCT} IN {SHOP_PRODUCTS} -->
<li>
<a href="{PRODUCT.CODE}">{PRODUCT.NAME|htmlspecialchars}</a>
— {PRODUCT.PRICE} ₽
</li>
<!-- ENDFOR -->
</ul>
<!-- END: MAIN -->
Ход рендеринга:
Cotpl_loop::__constructразбирает заголовок{PRODUCT} IN {SHOP_PRODUCTS}→$val = 'PRODUCT',$set = Cotpl_var('SHOP_PRODUCTS').- При
$t->parse('MAIN')вызываетсяCotpl_block::parseдляMAIN, который обходит дочерние блоки. Один из них —Cotpl_loop. Cotpl_loop::textвычисляет$set→ массив из$t->vars['SHOP_PRODUCTS'].is_array($set)—true.foreach:$tpl->assign('PRODUCT', $current).- Тело цикла рендерится с текущим
PRODUCT.{PRODUCT.NAME}→Cotpl_var::evaluate→$t->vars['PRODUCT']['NAME'].{PRODUCT.NAME|htmlspecialchars}— то же с фильтром. - Результат копится в локальной
$dataи возвращается.
Ограничение примера. Ссылка href="{PRODUCT.CODE}" даёт сырое значение CODE. Если нужен URL вида cot_url('shop', ['c' => $code]), его нужно вычислить в PHP до assign — тег с вызовом cot_url внутри FOR-итерации не сформирует URL автоматически.
6.3. Подход <!-- BEGIN --> + $t->parse()
Контроллер:
foreach ($products as $i => $p) {
$t->assign([
'SHOP_ROW_URL' => cot_url('shop', ['c' => $p['CODE']]),
'SHOP_ROW_TITLE' => htmlspecialchars($p['NAME'], ENT_QUOTES, 'UTF-8'),
'SHOP_ROW_PRICE' => $p['PRICE'],
'SHOP_ROW_ODDEVEN' => cot_build_oddeven($i),
]);
$t->parse('MAIN.SHOP_ROW');
}
$t->parse('MAIN');
$t->out('MAIN');
Шаблон:
<!-- BEGIN: MAIN -->
<ul class="shop-list">
<!-- BEGIN: SHOP_ROW -->
<li class="{SHOP_ROW_ODDEVEN}">
<a href="{SHOP_ROW_URL}">{SHOP_ROW_TITLE}</a>
— {SHOP_ROW_PRICE} ₽
</li>
<!-- END: SHOP_ROW -->
</ul>
<!-- END: MAIN -->
Ход рендеринга:
- При загрузке шаблона
Cotpl_block::compileрегистрируетMAIN.SHOP_ROWв$index. - Каждый
$t->parse('MAIN.SHOP_ROW')→XTemplate::parseнаходит блок, вызываетCotpl_block::parse. Метод рендерит содержимое блока с текущими$t->varsи добавляет строку вSHOP_ROW.$data. $t->parse('MAIN')→Cotpl_block::parseдляMAIN. Внутри дляSHOP_ROWвызываетсяCotpl_block::text, который склеивает N строк и очищаетSHOP_ROW.$data.$t->out('MAIN')→Cotpl_block::textдляMAINсклеивает и выводит.
6.4. Различия в примерах
- Контроллер: в FOR — одна строка
assignи одинparse. В BEGIN — N+1parse(N строк + финальный MAIN), Nassign. - Шаблон: в FOR теги с точками (
{PRODUCT.NAME}); в BEGIN — плоские ({SHOP_ROW_TITLE}). - URL и экранирование: в FOR нужно подготовить в PHP заранее или использовать
|-фильтры. В BEGIN готовятся в PHP на каждой итерации — естественно. - Регистр: в FOR ключи
NAME,PRICE,CODEдолжны быть в верхнем регистре, чтобы{PRODUCT.NAME}нашёл значение. В BEGIN регистр исходного массива не важен: PHP извлекает значение по своему ключу и присваивает плоский тег.
7. Разбор на примере: дерево категорий
Дерево — это вложенный список неопределённой глубины. Это задача, где проявляется принципиальное ограничение модели накопления CoTemplate.
7.1. Как деревья рендерятся в Cotonti
Стандартные модули Cotonti (в том числе структура страниц, page.list.php) рендерят дерево как плоский список с индикатором уровня. В контроллере обходятся категории, для каждой присваивается LIST_CAT_ROW_LEVEL (или аналогичный тег), и строки парсятся в один блок <!-- BEGIN: LIST_CAT_ROW -->. Вложенность отображается через CSS (отступы, иконки) — HTML остаётся плоским (<div class="list-row category-row"> внутри общего контейнера).
Это не «упрощение»: модель Cotpl_block рассчитана на накопление строк в одном буфере. Она не предоставляет механизма для динамической вложенности.
7.2. Почему <!-- FOR --> не подходит
Cotpl_loop::text не вызывает себя рекурсивно. Вложенный <!-- FOR --> компилируется в отдельный объект и работает для фиксированной глубины, но дерево с произвольным числом уровней требует динамической структуры, которую шаблон статически не описывает. Кроме того, parse на FOR-блоке бросает Exception — нельзя наполнить блок из PHP-рекурсии.
7.3. Что реально возможно с Cotpl_block
Cotpl_block::parse можно вызывать из рекурсивной PHP-функции. Однако накопление идёт в один общий $data в порядке вызовов. Рекурсивный обход в порядке «родитель → дети → внуки» даст плоский список, а не вложенный HTML:
function render_flat($t, $parentCode, $level = 0) {
foreach (get_children($parentCode) as $child) {
$t->assign([
'NODE_TITLE' => htmlspecialchars($child['title']),
'NODE_LEVEL' => $level,
'NODE_URL' => cot_url('shop', ['c' => $child['code']]),
]);
$t->parse('MAIN.NODE');
render_flat($t, $child['code'], $level + 1);
}
}
// Вызов: render_flat($t, ''); $t->parse('MAIN'); $t->out('MAIN');
Шаблон:
<!-- BEGIN: MAIN -->
<ul class="shop-tree">
<!-- BEGIN: NODE -->
<li class="level-{NODE_LEVEL}">
<a href="{NODE_URL}">{NODE_TITLE}</a>
</li>
<!-- END: NODE -->
</ul>
<!-- END: MAIN -->
Это работает: строки накапливаются в NODE.$data в порядке вызовов (родитель, его первый ребёнок, первый внук, …). HTML плоский; вложенность передаётся классом level-N.
Вложенный HTML. Чтобы получить <ul> внутри <li> для каждого уровня, необходимо, чтобы дочерний HTML был готов до вызова parse для родителя. Для этого дочерний HTML нужно сформировать как строку. Cotpl_block::text('MAIN.NODE') после рендера вернёт всё накопленное и очистит буфер — включая уже накопленные родительские строки. Изолировать поддерево через один общий блок нельзя. Рабочие варианты:
- Ручная сборка HTML в PHP — рекурсивная функция возвращает строку
<ul>…</ul>, которая присваивается тегу ($t->assign('TREE_HTML', $html)). Шаблон выводит{TREE_HTML}. Дерево строится в PHP, шаблонизатор для структуры не используется. - Отдельный
XTemplateна поддерево — избыточно и нестандартно. - Плоский список с уровнями — стандартный подход Cotonti, описанный выше.
Поэтому утверждение «рекурсия работает через <!-- BEGIN -->» требует уточнения: Cotpl_block позволяет вызывать parse из рекурсивной функции и накапливать строки, но нативная модель не поддерживает динамически вложенный HTML. Для настоящей вложенности нужна ручная сборка строк.
8. Сравнительный анализ
8.1. Где различаются
| Аспект | <!-- FOR --> |
<!-- BEGIN --> + $t->parse() |
|---|---|---|
| Где цикл | Cotpl_loop::text (в шаблонизаторе) |
foreach в PHP-контроллере |
| Источник | Только array (is_array($set)) |
Любой итерируемый: массив, Traversable, генератор |
Число вызовов $t->parse |
1 (MAIN) |
N+1 (N строк + финальный MAIN) |
| Синтаксис тегов внутри | {VAL}, {VAL.KEY}, {KEY} |
Плоские {ROW_TAG} |
| Регистр имён тегов (первый символ) | [A-Z0-9_$] (общее ограничение splitToChunks) |
То же |
| Регистр ключей исходного массива | Точное совпадение с сегментом тега (isset($var['NAME'])) |
Не важен (PHP извлекает значение) |
| Накопление между вызовами | Нет ($this->data не используется) |
Есть ($this->data) |
$t->parse на блоке |
Cotpl_loop::parse бросает Exception |
Cotpl_block::parse накапливает |
$t->reset на блоке |
Cotpl_loop::reset бросает Exception |
Cotpl_block::reset очищает |
Повторный out без parse |
Пусто (MAIN.$data очищен) |
Пусто (MAIN.$data очищен) |
Повторный parse('MAIN') + out |
Цикл выполнится снова → HTML снова | MAIN.$data пуст (если не было новых parse('MAIN.ROW')) → пусто |
| Вложенные циклы | Работают для фиксированной глубины | Работают |
| Динамическая вложенность (дерево) | Не поддерживается | Не поддерживается нативно; нужна ручная сборка HTML |
parse из PHP-рекурсии |
Невозможен (бросает Exception) |
Технически возможен, но даёт плоский список |
8.2. Где совпадают
| Аспект | Оба подхода |
|---|---|
| Финальный рендер | $t->parse('MAIN'); $t->out('MAIN'); |
| Хранилище локальных тегов | $t->vars |
| Глобальные теги | {PHP.var} из $GLOBALS |
| Синтаксис фильтров | {TAG|func} |
| Вложенность конструкций | IF, FOR, BEGIN вкладываются |
| Компиляция | Cotpl_block::compile |
| Кеш | self::$cacheDir . '/templates/' |
8.3. Принципиальное различие
Cotpl_loop — «одноразовый» цикл: получает массив, обходит, возвращает HTML. Не накапливает, не принимает внешнее управление через parse/reset, не рекурсивен.
Cotpl_block — «накопительный» блок: каждый parse добавляет строку в $this->data, text склеивает и очищает, reset очищает без вывода. Это позволяет наполнять блок из PHP в цикле, сбрасывать и наполнять заново. Динамическую вложенность HTML эта модель сама по себе не обеспечивает.
9. Руководство по выбору
9.1. Когда <!-- FOR -->
Когда выполняются одновременно:
- данные — массив (не объект, не генератор);
- на строку не нужна логика, кроме подстановки;
- все значения готовы в PHP (URL сформированы, текст экранирован);
- ключи в массиве записаны в верхнем регистре и совпадают с сегментами тегов;
- динамическая вложенность не требуется.
Типичные примеры: список стран, языков, валют, статических пунктов меню, предустановленных значений в форме.
9.2. Когда <!-- BEGIN --> + $t->parse()
Когда выполняется хотя бы одно условие:
- нужна логика на строку в PHP (
cot_url(),htmlspecialchars(),cot_build_oddeven(), проверки прав, склонения); - источник — объект или генератор;
- нужен контроль над накоплением (
$t->reset); - нужна совместимость со стандартными шаблонами Cotonti (
page.list.tpl,admin.structure.tpl); - нужен плоский список с уровнями (стандартный подход для деревьев).
Типичные примеры: список товаров с URL, список страниц, плоский список категорий с уровнями.
9.3. Таблица решений
| Задача | Выбор | Причина по коду |
|---|---|---|
| Страны в выпадающем меню | <!-- FOR --> |
Массив, без логики на строку |
Товары с cot_url() и экранированием |
<!-- BEGIN --> |
Логика на строку в PHP |
| Список с классом odd/even | <!-- BEGIN --> |
cot_build_oddeven() в PHP |
| Плоский список категорий с уровнями | <!-- BEGIN --> |
Накопление, уровень в теге |
| Вложенное HTML-дерево | Ручная сборка HTML в PHP | Модель Cotpl_block не даёт динамической вложенности |
| Стандартные шаблоны Cotonti | <!-- BEGIN --> |
Совместимость |
Нужен $t->reset на блоке |
<!-- BEGIN --> |
Cotpl_loop::reset бросает Exception |
Источник — Traversable |
<!-- BEGIN --> |
is_array($set) для Traversable = false |
Повторный parse('MAIN') + out |
<!-- FOR --> |
Цикл выполнится снова; BEGIN вернёт пусто без новых строк |
9.4. Пошагово: <!-- FOR -->
- Подготовить массив в PHP с UPPERCASE-ключами.
$t->assign('LIST', $array).- В шаблоне:
<!-- FOR {ITEM} IN {LIST} --> ... <!-- ENDFOR -->. - Внутри:
{ITEM.KEY},{ITEM.KEY|htmlspecialchars}. $t->parse('MAIN'); $t->out('MAIN');.
9.5. Пошагово: <!-- BEGIN -->
- В шаблоне:
<!-- BEGIN: ROW --> ... <!-- END: ROW -->. - В PHP:
foreachпо данным. - На каждой итерации:
$t->assign([...])с плоскими тегами. - На каждой итерации:
$t->parse('MAIN.ROW'). - После цикла:
$t->parse('MAIN'); $t->out('MAIN');.
9.6. Комбинирование
Оба подхода можно смешивать: снаружи <!-- BEGIN --> для крупных блоков, внутри — <!-- FOR --> для простого статического списка. Ограничения каждого класса при этом сохраняются.
10. Типичные ошибки
<!-- FOR --> выводит пустую строку. Источник не массив. Cotpl_loop::text проверяет is_array($set) и пропускает цикл. Решение: привести к массиву или перейти на <!-- BEGIN -->.
<!-- FOR --> выводит пустоту вместо значения поля. Регистр ключа не совпадает. Тег {ITEM.NAME} ищет $current['NAME'] через isset. Если в PHP ['name' => ...] — вернётся null. Решение: UPPERCASE-ключи или переход на <!-- BEGIN -->.
Тег со строчной первой буквой не выводится. Cotpl_data::splitToChunks распознаёт только теги, начинающиеся с [A-Z0-9_$]. {product.name} останется в выводе как текст. Решение: имена тегов начинать с заглавной буквы.
$t->parse('MAIN.SOME_FOR_BLOCK') бросает Exception. Cotpl_loop::parse явно бросает Exception('Calling parse() on a loop'). FOR-блок наполняется только при рендере родителя.
$t->reset('MAIN.SOME_FOR_BLOCK') бросает Exception. Cotpl_loop::reset бросает Exception('Calling reset() on a loop').
Повторный $t->out('MAIN') возвращает пустоту (для <!-- BEGIN -->). Cotpl_block::text очищает $this->data. Нужны новые $t->parse('MAIN.ROW') и $t->parse('MAIN').
Повторный $t->out('MAIN') возвращает пустоту (для <!-- FOR -->). MAIN.$data очищен первым text. Чтобы цикл отработал снова, нужен повторный $t->parse('MAIN').
Тег VAL/KEY после FOR сохраняет последнее значение. Cotpl_loop::text пишет в общий $t->vars. После цикла {PRODUCT} содержит последний элемент. Не используйте эти имена вне цикла.
Вложенный FOR «не видит» внешнюю переменную. Вложенный FOR компилируется в отдельный объект и получает $set через evaluate, читая $t->vars. Если источник внутреннего цикла — {A.sub}, он читает $t->vars['A']['sub'], где A присвоен внешним циклом. Это работает, но требует, чтобы структура была описана в шаблоне статически.
Рекурсивная функция с $t->parse('MAIN.NODE') даёт плоский список, а не вложенный HTML. Cotpl_block накапливает строки в одном буфере в порядке вызовов. Для вложенного HTML нужна ручная сборка строк в PHP.
Плагин присвоил тег, а следующий assign перезаписал его. $t->vars — общий массив; повторный assign с тем же именем затирает значение.
{PHP.var} не выводится. Для PHP 8.1+ $GLOBALS доступен только для чтения; Cotpl_var::evaluate для name === 'PHP' берёт $var = $GLOBALS, затем спускается по ключам. Если переменная не задана в $GLOBALS — вернётся null.
11. Словарь терминов
CoTemplate — шаблонизатор Cotonti, файл system/cotemplate.php. Читает .tpl, выполняет теги и конструкции, возвращает HTML.
XTemplate — главный класс. Публичное $vars, защищённые $blocks и $index. Методы assign, parse, text, out, reset.
Cotpl_block — класс блока <!-- BEGIN -->. Защищённое $data, публичное $blocks. parse накапливает, text склеивает и очищает, reset очищает.
Cotpl_loop — класс цикла <!-- FOR -->. Наследник Cotpl_block. parse и reset бросают Exception. text обходит массив, присваивает VAL/KEY, возвращает HTML.
Cotpl_var — класс тега. Разбирает |-фильтры и .-ключи. evaluate читает $t->vars (или $GLOBALS для PHP), спускается по ключам, применяет фильтры. Для массива — isset($var[$key]) с точным регистром.
Cotpl_data — класс фрагмента текста и тегов. splitToChunks распознаёт теги с первым символом [A-Z0-9_$]. text склеивает, для массива/объекта применяет var_export.
Cotpl_logical — класс <!-- IF -->. parse и reset бросают Exception. text вычисляет Cotpl_expr и рендерит ветку.
Cotpl_expr — выражение в <!-- IF -->. Разбирает в постфиксную запись, вычисляет.
$t->vars — публичный массив локальных тегов. Общий для всего шаблона, без областей видимости.
$t->blocks, $t->index — защищённые: корневые блоки и карта путей.
$this->data (в Cotpl_block) — защищённый массив накопленных строк.
$this->key, $this->val, $this->set (в Cotpl_loop) — имя тега ключа, имя тега значения, объект Cotpl_var источника.
assign($name, $val, $prefix) — присваивает тег или массив тегов. Регистр сохраняется, повторный вызов затирает.
parse($block) — рендерит блок с накоплением (для Cotpl_block). Для Cotpl_loop и Cotpl_logical бросает Exception.
text($block) — возвращает HTML. Для Cotpl_block склеивает и очищает $data; для Cotpl_loop выполняет цикл; для Cotpl_logical рендерит ветку.
out($block) — выводит text() в браузер.
reset($block) — очищает накопленное (для Cotpl_block). Для Cotpl_loop и Cotpl_logical бросает Exception.
Тег {NAME} — место подстановки. Локальный — из $t->vars, глобальный {PHP.var} — из $GLOBALS.
Плоский тег — {ROW_NAME}, используется во втором подходе.
Тег с точкой — {ITEM.NAME}, спуск по ключам; используется в первом подходе.
Фильтр |function — цепочка функций, применяемых к значению. Аргументы в скобках; $this — значение предыдущего шага.
<!-- FOR --> — цикл по массиву. Синтаксис: <!-- FOR {VAL} IN {SET} --> или <!-- FOR {KEY}, {VAL} IN {SET} -->.
<!-- BEGIN --> — именованный блок. Синтаксис: <!-- BEGIN: NAME --> ... <!-- END: NAME -->. Наполняется через $t->parse('PARENT.NAME').
<!-- IF --> — условие: <!-- IF expr --> ... <!-- ELSE --> ... <!-- ENDIF -->.
{FILE "path"} — включение файла на этапе подготовки шаблона.
Скаляр — int, float, string, bool. Не массив, не объект. Для Cotpl_loop::text скаляр — не источник цикла.
Массив — набор значений с ключами. Единственный источник для Cotpl_loop::text.
Traversable — интерфейс перебираемых объектов. is_array($obj) = false, поэтому Cotpl_loop его не обрабатывает; для Cotpl_block — допустимый источник (PHP делает foreach).
Объект — экземпляр класса. Cotpl_var::evaluate умеет читать свойства объектов через $var->{$key}.
Null — «нет значения». Если ключ не найден, Cotpl_var::evaluate возвращает null; Cotpl_data::text приводит к пустой строке.
UPPERCASE — соглашение Cotonti: имена тегов и ключей массивов — заглавными. Для FOR критично из-за точного isset; для BEGIN — для читаемости (извлечение делает PHP).
Накопление — механизм Cotpl_block: parse добавляет строку в $data, text склеивает и очищает.
Рекурсия — вызов функции из себя. В CoTemplate parse можно вызывать из рекурсивной PHP-функции, но накопление даёт плоский список. Для вложенного HTML нужна ручная сборка.
Кеш шаблонов — сериализованное дерево объектов в self::$cacheDir . '/templates/', включается опцией cache в XTemplate::init.
Скомпилированный шаблон — дерево объектов Cotpl_block, Cotpl_data, Cotpl_loop, Cotpl_logical, построенное из .tpl.
Рендеринг — превращение шаблона и данных в HTML через parse/text/out.
Заключение
Выбор между <!-- FOR --> и <!-- BEGIN --> + $t->parse() определяется не предпочтениями, а устройством классов Cotpl_loop и Cotpl_block.
<!-- FOR --> даёт кратчайший код для массива с готовыми значениями без логики на строку. Он не накапливает, не принимает parse/reset, работает только с array, требует точного совпадения регистра ключей.
<!-- BEGIN --> + $t->parse() — универсальный инструмент для случаев, где нужна логика на строку в PHP, не-массив источник, контроль над накоплением или совместимость со стандартными шаблонами Cotonti. Он накапливает строки, поддерживает reset, но не даёт динамической вложенности HTML: для деревьев стандартный подход — плоский список с уровнями, а настоящая вложенность требует ручной сборки строк в PHP.
Оба подхода сосуществуют. Задача разработчика — выбрать тот, чьи ограничения не мешают конкретному сценарию.