Cotonti CoTemplate: вывод списков через FOR и BEGIN (работа с циклами)

Два подхода к выводу списков в CoTemplate: цикл FOR и блок BEGIN с наполнением из PHP. Разбор по исходному коду, примеры, сравнение и рекомендации по выбору.

0 Опубликована Опубликовано в: Cotonti CMF > Cotonti - чтиво. Документация.

Вывод списков в шаблонах CoTemplate: два подхода, их различия и практическое применение

Различия и практическое применение в Cotonti способов обработки циклов на стороне шаблона в FOR {KEY}, {VAL} IN {SET} или в бизнес логике через foreach в PHP-контроллере.

Оглавление

  1. Введение
  2. Базовые понятия
  3. Устройство CoTemplate
  4. Подход первый: <!-- FOR -->
  5. Подход второй: <!-- BEGIN --> + $t->parse()
  6. Разбор на примере: список товаров
  7. Разбор на примере: дерево категорий
  8. Сравнительный анализ
  9. Руководство по выбору
  10. Типичные ошибки
  11. Словарь терминов

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 -->

Ход рендеринга:

  1. Cotpl_loop::__construct разбирает заголовок {PRODUCT} IN {SHOP_PRODUCTS} → $val = 'PRODUCT', $set = Cotpl_var('SHOP_PRODUCTS').
  2. При $t->parse('MAIN') вызывается Cotpl_block::parse для MAIN, который обходит дочерние блоки. Один из них — Cotpl_loop.
  3. Cotpl_loop::text вычисляет $set → массив из $t->vars['SHOP_PRODUCTS'].
  4. is_array($set) — true. foreach: $tpl->assign('PRODUCT', $current).
  5. Тело цикла рендерится с текущим PRODUCT. {PRODUCT.NAME} → Cotpl_var::evaluate → $t->vars['PRODUCT']['NAME']. {PRODUCT.NAME|htmlspecialchars} — то же с фильтром.
  6. Результат копится в локальной $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 -->

Ход рендеринга:

  1. При загрузке шаблона Cotpl_block::compile регистрирует MAIN.SHOP_ROW в $index.
  2. Каждый $t->parse('MAIN.SHOP_ROW') → XTemplate::parse находит блок, вызывает Cotpl_block::parse. Метод рендерит содержимое блока с текущими $t->vars и добавляет строку в SHOP_ROW.$data.
  3. $t->parse('MAIN') → Cotpl_block::parse для MAIN. Внутри для SHOP_ROW вызывается Cotpl_block::text, который склеивает N строк и очищает SHOP_ROW.$data.
  4. $t->out('MAIN') → Cotpl_block::text для MAIN склеивает и выводит.

6.4. Различия в примерах

  • Контроллер: в FOR — одна строка assign и один parse. В BEGIN — N+1 parse (N строк + финальный MAIN), N assign.
  • Шаблон: в 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') после рендера вернёт всё накопленное и очистит буфер — включая уже накопленные родительские строки. Изолировать поддерево через один общий блок нельзя. Рабочие варианты:

  1. Ручная сборка HTML в PHP — рекурсивная функция возвращает строку <ul>…</ul>, которая присваивается тегу ($t->assign('TREE_HTML', $html)). Шаблон выводит {TREE_HTML}. Дерево строится в PHP, шаблонизатор для структуры не используется.
  2. Отдельный XTemplate на поддерево — избыточно и нестандартно.
  3. Плоский список с уровнями — стандартный подход 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 -->

  1. Подготовить массив в PHP с UPPERCASE-ключами.
  2. $t->assign('LIST', $array).
  3. В шаблоне: <!-- FOR {ITEM} IN {LIST} --> ... <!-- ENDFOR -->.
  4. Внутри: {ITEM.KEY}, {ITEM.KEY|htmlspecialchars}.
  5. $t->parse('MAIN'); $t->out('MAIN');.

9.5. Пошагово: <!-- BEGIN -->

  1. В шаблоне: <!-- BEGIN: ROW --> ... <!-- END: ROW -->.
  2. В PHP: foreach по данным.
  3. На каждой итерации: $t->assign([...]) с плоскими тегами.
  4. На каждой итерации: $t->parse('MAIN.ROW').
  5. После цикла: $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.

Оба подхода сосуществуют. Задача разработчика — выбрать тот, чьи ограничения не мешают конкретному сценарию.

Комментарии отсутствуют
Добавление комментариев доступно только зарегистрированным пользователям
webitproff
webitproff

Оффлайн

Последняя авторизация: 20.10.2026 01:59

Обо мне кратко
Поддержка и разработка веб-проектов на CMF Cotonti: приватные мессенджеры через сайт, открытые и закрытые небольшие социальные сети, торговые площадки и маркетплейсы, портал биржи фриланса и услуг, каталоги товаров оптовых поставщиков, дропшиппинг-платформы, интернет-магазины и многое другое.
Смотреть разработки и скачать
Публичное портфолио моих работ и разработок
Телеграм для сообщений
@webitproff
Телеграм-канал
@s/aBuyFILE
Публичный профиль
  • Дата размещения 20.10.2026 03:00
  • Обновлено 20.10.2026 21:19

Похожие страницы

Файл cotemplate.php в Cotonti
1 Основное назначение файла cotemplate.php в системной папке Cotonti CMF — это реализация системы шаблонов (или движка
Циклы в шаблонизаторе CoTemplate в Cotonti
2 Оператор FOR (циклы) Оператор FOR позволяет обрабатывать массивы или диапазоны. Простой пример с
Шаблонизатор CoTemplate в Cotonti Siena CMF
3 Шаблонизатор CoTemplate является ключевой частью Cotonti Siena CMF (Content Management Framework) — свободно
Даты в CMF Cotonti. Запись, хранение и вывод (представление)
4 Ниже приведён системный обзор механики работы с датами в CMF Cotonti на основе структуры таблицы cot_pages и набора
Вывод дат в шаблонах Cotonti (Форматы и примеры в тегах)
5 Вывод дат в шаблонах CotontiНастройка формата отображения: локально и глобальноВ системе Cotonti все даты внутри базы