Минимум: Шаблонизатор

1.1K
.
предисловие (+/-)


Задача:
Рекурсивный шаблонизатор.

Решение:
Для тех, кто не в курсе, под рекурсивным шаблонизатором имеется в виду некое решение, позволяющее взять вот такие шаблоны:
<!DOCTYPE html>                                                                 
<html>                                                                          
    <head>                                                                      
        <!-- Подключение файла head.html -->                                                                                           
    </head>                                                                     
    <body>                                                                      
        <?= $content ?>                                                         
    </body>                                                                     
</html>


<title><?= $title ?></title>
<meta charset="utf-8" />

и сформировать из них такую страницу:
<html>                                                                          
    <head>                                                                      
        <title>Загловок</title>
        <meta charset="utf-8" />                                                                                      
    </head>                                                                     
    <body>                                                                      
        Тело                                                         
    </body>                                                                     
</html>

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

В PHP минимально (на мой взгляд) реализуется следующим образом:
<?php                                                                                                                                                  
/**
 * @param string $path Адрес шаблона.
 * @param array $assign Переменные, передаваемые шаблону.
 *
 * @return string Скомпилированный документ.
 */
function template($path, array $assign = []){                                      
  extract($assign);                                                             
  ob_start();                                                                      
  include($path);                                                               
  return ob_get_clean();                                                        
}


Используется так:
echo template('index.html', [                                                   
  'title' => 'Заголовок',                                                               
  'content' => 'Тело',                                                      
]);


Послесловие (+/-)
.
╭∩╮ (`-`) ╭∩╮
Каким образом в главный шаблон (layout) происходит подключение подчиненных шаблонов?
Сверху вниз (из лейаута инклюдим всю мелочь), или снизу вверх (как в Plates)?
.
AlkatraZ, сверху вниз. На деле тут вообще нет layout, ты подрубаешь страницу, а она должна подключить нужный ей head, на выходе получаются шаблоны вида:
<?= template('header.html') ?>
<div>
  Код самой страницы
</div>
<?= template('footer.html') ?>
.
╭∩╮ (`-`) ╭∩╮
# Delphinum (09.03.2017 / 14:01)
AlkatraZ, сверху вниз. На деле тут вообще нет layout, ты подрубаешь страницу, а она должна подключить нужный ей head, на выходе получаются шаблоны вида:

<?= template('header.html') ?>
<div>
Ко
Как я понял, и для основного шаблона и для всех инклюдов используется один маленький метод template() ?
.
AlkatraZ, да. Функция (не метод, методы у классов, а тут чистейшая процедурка, ибо минималистишно) template используется как при подключении корневого шаблона, так и в шаблонах для подключения других шаблонов. Вообще все, что в области видимости этой функции или в глобальной области, может быть использовано в шаблонах.
.
╭∩╮ (`-`) ╭∩╮
# Delphinum (09.03.2017 / 14:06)
AlkatraZ, да. Функция (не метод, методы у классов, а тут чистейшая процедурка, ибо минималистишно) template используется как при подключении корневого шаблона, так и в шаблонах для подключения других
Понял.
Ну тогда имеет смысл отделить "мух от котлет", сейчас объясню, именно такой подход я применял в шаблонизаторе, что писал для mobiCMS
---
Лучше сделать 2 функции: одну template() как описано выше, для инициализации самого шаблона, с передачей в него всех переменных, а вторую например tplinclude() применять именно для инклюдов.
Поясню почему...
ob_start() является довольно ресурсоемкой функцией. И нет смысла ловить буферы в подключаемых шаблонах: они все равно инклюдятся в материнский, образуя с ним единое целое, включая пространство имен и наследуют все переменные. В tplinclude() мы аргументом передаем только путь к подключаемому файлу, а сама функция до безобразия проста:
tplinclude($path){
  return include($path);
}
.
╭∩╮ (`-`) ╭∩╮
Да, разумеется можно было бы применять include() непосредственно в шаблоне.
Но это черновик функции такой простой. В реальном приложении функция может обрабатывать переключение тем оформления, проверять наличие файла шаблона и т.д.
.
Delphinum
AlkatraZ, с тем же успехом, можно предлагать использовать для подключения в шаблонах не: <?= tplinclude(...) ?>, а просто <?= include(...) ?> (написал уже после твоего предыдущего коммента)

Бывает полезно еще ограничивать область видимости подключаемого шаблона. То есть ты подключаешь шаблон, но передаешь в него специфические для данного шаблона переменные, а не всю область видимости, доступную базовому шаблону. Не знаю насколько это полезно, лично я никогда не сталкивался с проблемами "слишком большой области видимости". В моем решении можно сделать так: <?= template('head.html', [переменные только этого подшаблона]) ?>
.
Delphinum
AlkatraZ, вообще, если подумать, можно как то реализовать в функции template механизм определяющий место вызова этой функции, и если это шаблон, то не использовать ob_start, а если php скрипт, то использовать.

Как то так:
function template($path, array $assign = []){                                   
  static $bufferOff;                                                                
                                                                                
  extract($assign);                                                             
         
  // Если первый вызов, то используется ob_start                                                                       
  if(is_null($bufferOff)){                                                          
    $bufferOff = true;                                                             
    ob_start();                                                                 
    include($path);                                                             
    return ob_get_clean();                                                      
  }                                                                             
  // Для последующих вызовов буферизация не используется
  else{                                                                         
    include($path);                                                                                                                                    
  }                                                                             
}
.
╭∩╮ (`-`) ╭∩╮
Насчет области видимости и предотвращению конфликтов среди модулей разных разработчиков, уже давно подобное решено в сессиях. Наиболее простой подход в Aura
$var['vendor']['my_var']

То есть, ты гонишь в шаблон всего одну переменную, которая является массивом, или инстансом ArrayObject. А там уже разделяешь, каждый вендор выбирает себе какой-то верхний уровень и далее клепает нужные переменные на нижнем уровне.
Это все решаемо на этапе проектировки общего приложения. Далее, для разработчиков ты публикуешь правила.

Итог: максимальная простота и скорость кода. Страницу вцелом легко кэшировать, или же, отловя буфер передавать результат куда нибудь в Response если применяется что-то крутое.
Всего: 28