Albireo CMS 2025.01.17

TPL-шаблонизатор в Albireo CMS

TPL-шаблонизатор используется для того, чтобы упростить использование связки PHP+HTML. Хотя PHP сам по себе неплохой шаблонизатор, но из-за того, что он используется символы < и >, это создаёт определённую путаницу с этими же символами в HTML.

Другая возможность TPL-шаблонизатора в том, чтобы упростить вывод готовых данных в цикле, где формируется html-разметка.

Для работы с TPL-шаблонизатором требуются хотя бы начальные знания PHP.

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

Существенное отличие от этих проектов заключается в том, что шаблонизатор Albireo CMS не пытается заменить PHP-код, а работает как дополнительные возможности PHP. Именно поэтому в Albireo CMS в шаблонизаторе можно использовать абсолютно любой php-код без ограничений.

Что касается синтаксиса TPL-шаблонизатора в Albireo CMS, то он ориентируется на синтаксис фигурных скобок — это уже привычный для программистов подход.

Основные принципы

Работа TPL-шаблонизатора заключается в двух действиях. Первое — это сформировать исходные данные для вывода и второе — передать их в tpl-шаблон.

Поскольку практически все tpl-шаблоны используют css-классы шаблона, то их следует размещать в каталоге шаблона tpl.

Формально же, это не обязательное правило и эти файлы можно размещать в произвольном месте вашего сайта.

Чтобы понять принцип действия TPL-шаблонизатора покажу небольшой демо-пример.

В теле страницы (например в hello.php) создадим массив данных для вывода:

# массив
$myData = [
    'h1' => 'lorem',
    'h2' => 'ipsum',
    'h3' => 'dolor',
];

# отправим его на вывод
echo tpl(data: $myData, tpl: TPL_DIR . 'my.php');

Файл my.php:

echo $h1;
echo $h2;
echo $h3;

Если обновить страницу, то мы получим вывод loremipsumdolor.

Таким образом $myData был распакован на php-переменные, которые совпадают с его ключами.

Файл my.php представляет собой обычный php-файл, именно поэтому мы используем чистый php-код. Но если нам нужно использовать html-разметку, то код будет сложнее читать и именно здесь можно использовать синтаксис TPL-шаблонизатора.

Изменим my.php:

<h1>{{ $h1 }}</h1>
<h2>{{ $h2 }}</h2>
<h3>{{ $h3 }}</h3>

Теперь это не php-код, а обычный текст с html-тэгами. Конструкция {{ }} эквивалента echo, то есть шаблонизатор сам сделал замены текста на php-код и выполнил его.

Если исходные данные содержат html-спецсимволы, например кавычки и угловые скобки, то можно использовать другой вариант вывода:

<h1>{* $h1 *}</h1>
<h2>{* $h2 *}</h2>
<h3>{* $h3 *}</h3>

В этом случае помимо echo шаблонизатор прогонит вывод через функцию htmlspecialchars().

Если в tpl-файле нужно оставить комментарий, то он обрамляется через {# #}:

{# вывод заголовков #}

<h1>{* $h1 *}</h1>
<h2>{* $h2 *}</h2>
<h3>{* $h3 *}</h3>

Если же нужно вывести текст без обработки, например код JavaScript, то его следует обрамить в {@verbatim}:

{# вывод заголовков #}

<h1>{* $h1 *}</h1>
<h2>{* $h2 *}</h2>
<h3>{* $h3 *}</h3>

{@verbatim}
код без обработки
{@endverbatim}

Хотя в тексте tpl-файла можно использовать обычные php-вставки, иногда удобней использовать их замены:

{@ ваш php-код @} -> эквивалентно <?php ваш php-код ?>
{?php ваш php-код ?} -> эквивалентно <?php ваш php-код ?>

Если ключ исходного массива будет содержать символы, которые не могут быть именами переменных, то шаблонизатор их заменит символом _.

$myData = [
    'image-home' => 'адрес',
];

В этом примере в tpl-файле будет создана переменная $image_home, потому что $image-home — это некорректное имя переменной в PHP.

Вложенные циклы

Частой задачей является выполнение вложенных циклов. Например у нас есть массив страниц и нужно каждую из них вывести через tpl-шаблонизатор.

Схематически это можно представить как многомерный массив:

$myPages = [
    'file1.php' => [
        'slug' => 'file1',
        'title' => 'Страница 1',
    ],
    
    'file2.php' => [
        'slug' => 'file2',
        'title' => 'Страница 2',
        ],
    ];

Для того, чтобы вывести этот массив потребуется два вложенных цикла. Внешний будет перебирать ключи file1.php и file2.php, а внутренний будет проходится по элементам title и slug.

С помощью TPL-шаблонизатора это делается достаточно просто. Рассмотрим демо-пример на рабочих данных вашего сайта.

Вначале получим страницы сайта — они как раз представляют собой аналогичный многомерный ассоциативный массив.

// получим записи с type: blog
$files = getPagesField('type', 'blog'); 

// отсортируем их, чтобы новые оказались вверху
$files = sortArray($files, 'date', 'date-desc');

// возьмем срез из первых 3-х записей
$files = array_slice($files, 0, 3, true);

# pr($files); // можно посмотреть содержимое массива для отладки

// отправим эти данные на вывод
echo tpl(data: $files, tpl: TPL_DIR . 'my.php');

Теперь напишем код my.php:

{@loop $DATA}
    <h1>{* $title *}</h1>
    <p class="t-italic">{{ $description }}</p>
    <hr>
{@endloop}

Код @loop запускает цикл по $DATA — это «внешняя» часть массива $files. Поэтому внутри конструкции @loop станут доступны переменные из «внутреннего» цикла. В нашем случае это поля title и description.

Проверки isset и empty

Сейчас мы не проверяем существование переменных, предполагая, что они есть, но в большинстве случаев нужно проверить их существование. Для этого используется конструкция @isset:

{@loop $DATA}
    <h1>{* $title *}</h1>
    
    {@isset $announce}
        <p class="t-italic">{{ $announce }}</p>
    {@endisset}
    <hr>
{@endloop}

Здесь мы проверяем переменную $announce (поле announce) и выводим только если она есть.

Другой способ проверки — это условие @empty, которое не только проверяет существование переменной, но и его ненулевое значение (пустая строка, 0, false, null).

{@loop $DATA}
    <h1>{* $title *}</h1>
    
    {@empty! $announce}
        <p class="t-italic">{{ $announce }}</p>
    {@endempty}
    <hr>
{@endloop}

В данном случае {@empty! $announce} — это !empty(), поскольку нам нужно вывести если не пусто.

Управляющие конструкции if

Команды @isset и @empty — это частный случай команды @if:

{@if xxx} -> if (xxx) :
{@if! xxx}  -> if (!xxx) :

{@else} -> else :
{@elseif $a} -> elseif ($a) :
{@elseif! $a} -> elseif (!$a) :

{@endif} -> endif

{@isset $a} -> if (isset($a)) :
{@isset! $a} -> if (!isset($a)) :
{@endisset} -> endif
 
{@empty $a} -> if (empty($a)) :
{@empty! $a} -> if (!empty($a)) :
{@endempty} -> endif

С их помощью можно создавать достаточно сложные конструкции:

{@loop $DATA}
    <h1>{* $title *}</h1>
    
    {@empty! $announce}
        <p class="t-italic">{{ $announce }}</p>
    {@else} 
        {@empty! $description}
            <p class="t-italic">{{ $description }}</p>
        {@else}
            <p class="t-red">Нет описания...</p>
        {@endempty}
    {@endempty}
    
    <hr>
{@endloop}

Счетчики цикла loop

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

$loopCount - всего элементов в цикле
$loopKey - текущий ключ
$loopIndex - текущий счётчик
$loopRemaining - обратный счётчик
$loopOdd - true если это нечётный элемент
$loopEven - true если это чётный элемент
$loopFirst - true если это первый элемент
$loopLast - true если это последний элемент

Например мы не хотим выводить тэг HR после последней записи, а также вывести номер записи в заголовке:

{@loop $DATA}
    <h1>{{ $loopIndex }}. {* $title *}</h1>
    
    {@isset $announce}
        <p class="t-italic">{{ $announce }}</p>
    {@endisset} 
    
    {@if! $loopLast}<hr>{@endif}
{@endloop}

Flex-сетка с помощью цикла loop

Сетка flex состоит из общего контейнера и вложенных ячеек. Поэтому контейнер нужно вынести из цикла @loop.

<div class="flex flex-wrap">
    {@loop $DATA}
        <div class="w30 w100-tablet">
            <h3>{* $title *}</h3>
            
            {@isset $announce}
                <p class="t-italic t90">{{ $announce }}</p>
            {@endisset} 
        </div>
    {@endloop}
</div>

Здесь мы создаём flex-контейнер, а для ячеек выставляем размер 30% ширины для десктопа и 100% ширины для мобильных устройств. То есть получается три ячейки в ряд.

Управление class и style

С помощью @class можно управлять выводом css-классов. Конструкция состоит из массива, где указываются классы и условие их вывода. Например сделаем красный текст для каждого четного элемента:

{@loop $DATA}
    <h2 class="{@class ['t-red' => $loopEven]}">{* $title *}</h2>
{@endloop}

Если элемент массива содержит только ключ, то он выводится при каждой итерации:

{@loop $DATA}
    <h2 class="{@class ['t-italic', 't-red' => $loopEven]}">{* $title *}</h2>
{@endloop}

Этот код будет эквивалентен:

{@loop $DATA}
    <h2 class="t-italic {@class ['t-red' => $loopEven]}">{* $title *}</h2>
{@endloop}

Аналогично работает конструкция @style:

{@loop $DATA}
    <h2 style="{@style ['color: red' => $loopEven]}">{* $title *}</h2>
{@endloop}

Генератор yield

С помощью TPL-шаблонизатора довольно просто организовать перебор элементов. Например нужно для каждой записи использовать свои наборы классов. Для этого удобно применить генератор @yieldN, где N — это произвольный номер генератора.

{@loop $DATA}
    <h2 class="{@yield1 ['t-red', 't-green', 't-blue']}">{* $title *}</h2>
{@endloop}

В данном случае для первого вывода будет использован класс t-red, для второго — t-green, для третьего — t-blue, а для всех последующих ничего не будет выведено.

Генератор @yield обязательно нумеруется, поскольку таких генераторов может быть множество и нужно их различать.

Проверка полей

Частая задача — проверить отмечено ли поле записи. Например есть поле image-noview, которое должно принудительно отключать вывод миниатюры (поле image-large). Для такой проверки используется конструкция @check:

...
    {@isset $image_large}{@check! $image_noview}
        <a href="{{ $page_url }}">{{ hThumb($image_large, width: 900, height: 471, attr: ['class'=>'w100 rounded10 hover-img-gray30']) }}</a>
    {@endcheck}{@endisset}
...

Здесь первое условие — проверка существования переменной $image_large и если оно есть, то проверяется отметка {@check! $image_noview}, которое будет соответствовать полю image-noview: -.

Если указать {@check $image_noview}, то это будет соответствовать image-noview: +.

Условия match

Это новая возможность PHP 8, которая позволяет использовать сразу несколько условий в одном операторе.

{@loop $DATA}
    <h2>{* $title *}</h2>
    
    <p class="t-gray">{@match 
        $loopIndex == 1 => "Первый",
        $loopIndex == 2 => "Второй",
        $loopIndex == 3 => "Третий",
        default => '',
    }</p>
{@endloop}

Вывод контента файла

Если нужно вывести весь файл страницы целиком, то используется @contentFile:

{@loop $DATA}
    <div>{@contentFile}</div>
{@endloop}

Обратите внимание, что @contentFile выводит уже полностью готовый текст страницы, включая его заголовок и т.п.

В Albireo CMS эта возможность используется для вывода short-страниц.

Циклы for, foreach, while

Эти циклы повторяют стандартные PHP.

{@foreach $DATA as $url => $num} -> foreach ($DATA as $url => $num) :
{@endforeach} -> endforeach

{@foreach $DATA as $d}
   {{ tpl(TPL_DIR . 'loop.php', $d) }}
{@endforeach}

{@for $i = 0; $i < 10; $i++} -> for ($i = 0; $i < 10; $i++) :
{@endfor} -> endfor

{@for $i = 0; $i < 10; $i++}
    The current value is {{ $i }}
{@endfor}

{@while $i <= 10} -> while ($i <= 10) :
{@endwhile} -> endwhile

{@continue}  -> continue
{@break} -> break

{@continue $a} -> if ($a) continue
{@continue! $a} ->  if (!$a) continue

{@break $a} -> if ($a) break
{@break! $a} -> if (!$a) break

{@ $i = 1 @}
{@while $i <= 10}
    {{ $i }}
    {% $i++ %}
{@endwhile}

Языковой перевод

Если стоит задача сделать языковой перевод, то следует использовать @lang:

...
    {@isset $announce}
        __(mar10-tb) {{ $announce }} 
        __(t-right)  <a class="button icon-arrow-right" href="{{ $page_url }}">{@lang Read}</a>
    {@endisset}
...

В данном примере фраза «Read» будет пропущена через функцию lang() и вернёт её перевод (если он есть) для текущего языка пользователя. Для русского это будет «Читать», для украинского «Читати».

Дополнительные данные для tpl-файла

В некоторых случаях нужно передать в tpl-файл дополнительные данные, которые не будут связаны с исходным массивом данных. Например нужно сделать вывод заголовка блока.

Для этого он указывается в функции tpl() в аргументе add как массив:

echo tpl(data: $files, tpl: TPL_DIR . 'my.php', add: ['header' => 'Заголовок блока']);

Соответственно в tpl-файле ключи будут доступны как одноимённые переменные:

{@empty! $header}
    <h1>{* $header *}</h1>
{@endempty}

{@loop $DATA}
    <h2>{* $title *}</h2>
    ...
{@endloop}

Примеры вывода блоков

Tpl-файлы в первую очередь используются для формирования вывода записей. Есть разные алгоритмы вывода записей, поэтому для удобства вы можете посмотреть страницу ваш-сайт/group-samples, где приведены рабочие примеры. Исходный код этого адреса размещен в каталоге страниц в specific/group-samples.php.

Философия TPL-шаблонизатора Albireo CMS

В отличие от других шаблонизаторов, Albireo CMS не ставит никаких условий и ограничений по использованию любого кода. Задача в том, чтобы предложить разработчику шаблона более удобный инструмент для интеграции программной логики PHP в HTML-разметку. Именно поэтому в TPL-шаблонизаторе Albireo CMS нет неочевидных замен и ограничений на использование php-кода, как это принято в других шаблонизаторах (например в Twig запрещено выполнять свой php-код).

Если возможностей шаблонизатора Albireo CMS недостаточно, то вы всегда можете использовать нативный PHP.