Pull to refresh

Шаблонизация в PHP при помощи лямбда-функций и замыканий

Reading time6 min
Views4.7K
Начиная с php 5.3, мы получили замечательную возможность использовать замыкания и анонимные функции. Они, вместе с альтернативным синтаксисом, очень удобны для использования при шаблонизации (конечно, за исключением случаев, когда верстальщику не надо давать доступ к php), а шаблоны на их основе быстры, легко переводятся в байткод акселератором, могут поддерживать блочное наследование, не требуют компиляции и кеширования, поддерживают скины и очень удобны в разработке.
Предполагается, что читатель имеет опыт работы с шаблонизаторами, например twig. Подробности под катом.

Для начала определимся с семантикой и синтаксисом.
1) Лямбда-функции в шаблоне назовем инструкциями, и будем писать их прописными буквами;
2) Системные и служебные переменные в шаблоне начинаются с символа подчерквания;
3) Все остальные переменные в шаблоне являются его непосредственными агрументами и пишутся со строчной буквы.

Шаблонизатор является объектом-сервисом, имеющем метод рендера:
public function exec($_template,array $_data=array(),$_skin=null,$_type='php',&$_buffer=null) {
}

В неймспейсе этого метода и будет происходить всё самое интересное.
Для начала, определим переменные и объявим в нём несколько инструкций с замыканиями, в конце добавим обработку результата:
public function exec($_template,array $_data=array(),$_skin=null,$_type='php',&$_buffer=null) {
            if (!isset($_skin)) $_skin = $this->api->cfg['default_skin'];
            if (!$_filename = $this->getFile($_template,$_skin,$_type)) return '';
            $_parent = null;
            $_api = $this->api;

            // Включает в шаблон ссылку на имеющий к нему отношение ресурс
            $R = function($name) use ($_template, $_skin) {
                echo "/res/t/{$_skin}/{$_template}/$name";
            };
            
            // Начинает наследуемый блок шаблона
            $BEGIN = function($blockname) {
                ob_start();
            };
            
            // Заканчивает наследуемый блок шаблона
            $END = function($blockname) use (&$_buffer,$_parent) {
                if (isset($_buffer[$blockname])) {
                    ob_end_clean();
                    echo $_buffer[$blockname];
                } else {
                    $_buffer[$blockname] = isset($_parent)?ob_get_clean():ob_get_flush();
                }
            };
            
            // Указывает имя расширяемого шаблона
            $EXTEND = function($template,$type=null) use (&$_parent) {
                if ($template) $_parent =array($template,$type);
            };
                
            // Включает другой шаблон внутрь шаблона
            $INCLUDE = function($template,$type=null) use ($_data,$_api,$_skin) {
                if ($template) echo $_api->templater->exec($template,$_data,$_skin,$type);
            };
                
            // Генерирует css-класс для оберточного dom-элемента
            $CLASS = function() use ($_template,$_skin) {
                echo "t-{$_template} s-{$_skin}";
            };
            if (!isset($this->instructions)) $this->instructions = $this->getInstructions();

            // Вставляет переменную или дефолтное значение
            $V = function(&$var,$default='',$raw=false) use ($api) {
                if (isset($var)) {
                    if (is_scalar($var)) echo $raw?$var:htmlspecialchars($var);
                    else $api->templater->dump($var);
                } else {
                    echo $raw?$default:htmlspecialchars($default);
                }
            };

            // Возвращает переменную или дефолтное значение
            $GV = function(&$var,$default='') {
                if (isset($var)) return $var;
                else return $default;
            },

            extract($_data);
            
            // Немного магии - работа с языко-зависимыми строками (массив строк, ключом является язык)
            if (!isset($_language)) $_language = $this->api->cfg['default_language'];
            $L = function(&$stringhash,$default=array('?')) use ($_language,$_api) {
                if (!isset($stringhash)) $stringhash = $default;
                if (is_string($stringhash)) {
                    echo htmlspecialchars($stringhash);
                    return;
                }
                if (isset($stringhash[$_language[0]])) echo htmlspecialchars($stringhash[$_language[0]]);
                elseif (isset($stringhash[$_api->cfg['default_language'][0]])) echo htmlspecialchars($stringhash[$_api->cfg['default_language'][0]]);
                else echo htmlspecialchars(reset($stringhash));
            };
            
            extract($this->instructions);
            ob_start();
            include $_filename;
            $content = ob_get_clean();
            if ($_parent) $content = $this->exec($_parent[0],$_data,$_skin,$_parent[1],$_buffer);
            return $content;
}


Наследование и переопределение блоков работает следующим образом:
1) Если в начале шаблона задана инструкция $EXTEND(), то расширяемый шаблон задается «предком» текущего шаблона.
2) При начале наследуемого блока ("$BEGIN()") открывается буфер записи. Имя в параметре нужно только для семантики;
3) При завершении наследуемого блока буфер записи выбрасывается в аутпут, или же выбрасывается существующий запомненный буфер, если он был заполнен (здесь нюанс: выполняются оба блока — и предка, и потомка, но выводится один; на практике это не существенно);

Таким образом, сначала рендерится потомок, потом предок. Если в предке есть блоки, определенные в потомке, то они не выводятся, а заменяются блоками потомка. При этом скин предка берется из скина потомка (задается при вызове рендера). Могут быть более двух уровней связи «предок-потомок», в результате функции рендера будет только последний блок с заданным именем.

Инструкции $V, $L и $GV принимают только переменные (с использованием разрешения на изменение "&", которое нужно для того, чтобы исключить предупреждения обрщения к несуществующей переменной; переменная внтури лямбда-функции будет равна null)

Инструкция $R предназначена для вывода ресурсов, привязанных к шаблону. Например так:
<?foreach($GV($pictures,array()) as $picture):?>
     <img src="<?$R($picture)?>"/>
<?endforeach;?>

Дополнительные инструкции (например, работа с константами и т.д.) могут быть переданы в метод через extract($this->instructions), этот массив лямбда-функций формируется динамически.

Пример шаблона (class используется в теге html с целью указания в одном из скинов height: 100%, например):
<!DOCTYPE html>
<html class="<?$CLASS()?>" id="<?$V($_id)?>">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
        <title><?$L($title,array('en'=>'No title','ru'=>'Нет заголовка'))?></title>
        <link rel="stylesheet" href="/res/var/t.css?<?=$_api->build?>" type="text/css" media="all" />
        <script type="text/javascript" src="/res/jquery.js?<?=$_api->build?>"></script>
        <script type="text/javascript" src="/res/var/t.js?<?=$_api->build?>"></script>
        <script type="text/javascript" src="/res/var/frontend.js?<?=$_api->build?>"></script>
    </head>
    <body>
        <header>
<?$BEGIN('header')?>
            <h1><?$L($title,array('en'=>'No title','ru'=>'Нет заголовка'))?></h1>
<?$SLOT($langswitch)?>
<?$END('header')?>
        </header>
        <section>
<?$BEGIN('content')?>
            <?$L($content,array('en'=>'No content','ru'=>'Нет контента'))?> 
<?$END('content')?>
        </section>
        <footer>
<?$BEGIN('footer')?>
<?$SLOT($menu_footer)?>
            <p class="copy">
                 <?=date('Y')?> MyProject
            </p>
            <p class="info">
                <?$s=array('en'=>'Generated','ru'=>'Создано');$L($s)?>: <?=date('r')?> 
            </p>
<?$END('footer')?>
        </footer>
    </body>
</html>


Пример использования шаблонизатора при сборке css:
. {background: url("<?$R('bg.png')?>")}
. > .left {width: <?$C('left-margin')?>; border: 1px <?$C('border-color')?> solid;} 

Здесь $C — инструкция для работы с константами. Благодаря шаблонизации, в стилевых файлах можно делать циклы, встраивать функции расчета координат, расширять css-ки, инклудить на стороне сервера, и многое другое интересное. Стили начинаются с точки, потому что сборщик css заменяет точку в начале строку на селектор блока шаблона автоматически, таким образом поддерживается блочная верстка.

Точно так же можно пропускать через шаблонизатор javascript-файлы при сборке компонентов проекта. Например, это удобно для подстановки путей к ajax-запросам и прочим линкам с помощью инструкции $PATH('route_name',$args).

Все ресурсы шаблонов должны быть собраны сборщиком в папки докрута, а все css-ки и js-ки шаблнов предворены префиксными селекторами (см. "$CLASS()") и склеены в один (ну, два) файла в докруте. Об этих механизмах я расскажу отдельно в других статьях.

Отмечу, что данный подход существует в прототипе в рамках работающей системы, которую я целиком, по понятным причинам, в статье описывать не буду, и является эксперементальным. Основной целью было сокращение потерь времени на разработку из-за долгой компиляции (twig) большого количества шаблонов, при сохранении приемлемого быстродействия. Поэтому с радостью почитаю о нюансах и подводных камнях такого подхода от имеющих опыт людей в комментариях.
Tags:
Hubs:
+5
Comments19

Articles