Pull to refresh

Ещё один php шаблонизатор

Reading time 9 min
Views 13K

Доброго времени суток,

Хочу рассказать о своём шаблонизаторе для проектов на PHP.
Понимаю, что рискую быть обвинённым в изобретении велосипеда, поэтому объясню свои мотивы: Большинство шаблонизаторов меня не устраивают изначально, среди них Smarty, Quicky и все им подобные, причина — мне кажется, что шаблонизатор должен избавлять от использования логики в шаблонах, а не навязывать свой синтаксис для той же логики.
Иначе говоря, такой:
  1. {?$x = 2+2}
, или такой
  1. {foreach name=my from=array('One','Two','Three') key="i" item="text"}
подходы для меня абсолютно неприемлимы!
Пожалуй, из всех шаблонизаторов больше всех удовлетворяет моим требованиям xtemplate, но у него есть целый ряд недостатков которые меня раздражают, например то, что все страницы нужно обрамлять в блоки, или то, что он интерпретирует шаблоны, а не компилирует, благодаря чему скоростью похвастаться не может. Ну и последнее — я решил написать шаблонизатор так, чтобы не было никаких проблем с добавлением функционала, а также, чтобы он был совместим с нативным шаблонизатором, который я использовал до этого, и к которому привык. Дело в том что конструкция 
  1. $tpl->assigned_var='abc';
которую часто используют нативные шаблонизаторы, мне нравится гораздо больше чем что-нибудь вроде:
  1. $thl->assign('assigned_var','abc');
В один прекрасный момент я понял, что проще написать свой шаблонизатор, чем искать тот, который мне подойдет. И, думаю, оказался прав, ведь дело обошлось несколькими вечерами.
Вообще говоря, процесс мне показался довольно интересным, и появилось много моментов, которые хотелось бы обсудить с сообществом.

Начну с описания синтаксиса:


1) Переменные:

Тут всё как обычно, разве что никаких "{$"
Бизнес логика Шаблон
  1. $tpl->var_name='...';
  1. {var_name}
  1. $tpl->var_name['sub_var']='...';
  1. {var_name.sub_var}

2) Блоки:

Нужны они, чтобы избавиться от конструкций типа {foreach name=my from=array ('One','Two','Three') key=«i» item=»text"}
Наподобие xtpl, вот только слегка автоматизировано, а именно, чтобы распарсить блок (еще говорят растиражировать), достаточно просто передать шаблону массив с данными!
Бизнес логика Шаблон
  1. $tpl->block_name[]['num']='4';
  2. $tpl->block_name[]['num']='8';
  3. $tpl->block_name[]['num']='15';
  1. <!--begin:block_name-->
  2. {block_name.num}
  3. <!--end:block_name-->
  1. $tpl->words['block']=array(
  2.     O=>array('word'=>'A'),
  3.     1=>array('word'=>'B'),
  4.     2=>array('word'=>'C'),
  5. );
  1. <!--begin:words.block-->
  2. {words.block.word}
  3. <!--end:words.block-->
Чтобы вывести переменную блока внутри — нужно назвать её {имя_блока.имя_переменной}
Это позволяет обращаться как к переменным блока, так и ко внешним переменным изнутри
Блок может быть абсолютно любой переменной, например, на втором примере блок строится по элементу «block» массива «words»
Так же блок может быть внутри другого блока, вот, например, простой способ построить таблицу умножения:
Бизнес логика Шаблон
  1. for ($i=1; $i<10; $i++)
  2.     for ($j=1; $j<10; $j++)
  3.         $tpl->table[$i]['row'][$j]['num']=$i*$j;
  1. <table>
  2.     <!--begin:table-->
  3.     <tr>
  4.         <!--begin:table.row-->
  5.             <td>{table.row.num}</td>
  6.         <!--end:table.row-->
  7.     </tr>
  8.     <!--end:table-->
  9. </table>

3) Проверки:

По сути, разновидность блоков. Нужна она, чтобы на основе какой-нибудь переменной либо показывать то что внутри, либо нет. Понятнее станет на примере:
Бизнес логика Шаблон
  1. $tpl->f_text=true;
  1. <!--if:f_text-->
  2. Пока f_text==true мы будем видеть этот текст
  3. <!--end:f_text-->
  1. $tpl->f_text=false;
  1. <!--if:f_text-->
  2. Тут можно писать что угодно, потому что заказчик не увидит
  3. <!--end:f_text-->

4) Функции:

Это скорее экспериментальная фича, хотелось бы услышать мнение, имеет ли право на жизнь такой подход:
Файл tpl.class.php Шаблон
  1. function up($text) {
  2.     return strtoupper($text)
  3. }
  1. {up}текст который нужно сделать БОЛЬШИМ{/up}
Обращаю внимание, что функции следует добавлять именно в класс шаблонизатора.
На данный момент функции работают только с одним параметром, и я думаю как следует расширить количество параметров — так:
  1. {func(param2,param3)}param1{/func}
, или так:
  1. {func}param1|param2|param3{/func}
или как-то ещё. Пока склоняюсь к первому варианту, его проще реализовать!

Теперь о принципах:


Шаблонизатор я решил разделить на две части:

1) Сам шаблонизатор (максимально компактный, всё самое нужное)
2) Компилятор (а вот тут вот всё остальное)
Это необходимо для повышения производительности, ведь не имеет никакого смысла в 8 кб кода компилятора, если шаблон уже скомпилирован и с тех пор не менялся.

Много подумать заставил процесс инклуда внутри шаблонов:

На первый взгляд, момент может показаться пустяковым, но это не так. Вообще говоря, инклуды пришлось разделить на две части — статические и динамические. Статический инклуд — это обычный инклуд, например
  1. <!--include:some_page.html-->
Такой инклуд обработается следующим образом — на его места вставится код из some_page.html, время изменения у файла откомпилированного шаблона будет на 1 секунду больше чем у самого шаблона, и из этого шаблонизатор узнает что нужно подключить специальный, также созданный компилятором файл, в который будет добавлена следующая строчка:
  1. if (filemtime('./some_page.html') != 1237369507) $needCompile=true;
Таким образом, при изменении этого файла — весь шаблон будет перекомпилирован.
Зачем это нужно, почему не вставить просто инклуд? А что если потребуется выводить блок из 1000 строк, внутри которого для удобства будет вставлен инклуд? Тогда такой фокус очень существенно поможет производительности!
Теперь о другом типе инклудов — динамические. Выглядит это чудо в моём шаблонизаторе например вот так:
  1. <!--include:{page_name}.html-->
То есть мы инклудим не какой-нибудь заранее указанный файл, а берём его имя или часть имени из переменной! Иногда может быть очень удобно, но старый способ при таком подходе уже не прокатит, ведь нужно чтобы при изменении в бизнес логике переменной, инклудился уже другой файл, поэтому такая конструкция откомпилируется в следующий код:
  1. <?php $this->render(''.$this->page_name.'.html');?>
Замечу что на данный момент такой инклуд не будет работать внутри блока, то есть работать будет, но внутри подключённого файла переменные блока будут недоступны, но думаю это не очень страшно, ведь у меня в отличие от xtpl блоки нужны только для циклического вывода какого-нибудь массива.

Php код внутри:

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

Ну и, наконец, какими принципами я руководствовался решая, какой будет синтаксис:

1) Минимум логики, всю логику — бизнес логике
2) Всё как можно естественнее
3) Меньше кода
4) Максимум возможностей

Код самого шаблонизатора:

  1. <?php
  2. class tpl {
  3.     function tpl($tplDir,$tmpDir) {
  4.         $this->tplDir=$tplDir;
  5.         $this->tmpDir=$tmpDir;
  6.     }
  7.     function Render($Path) {
  8.         $tmpName='tpl_'.str_replace(array('/','\\'),'.',$Path).'.php';
  9.         $tmpPath=$this->tmpDir.'/'.$tmpName;
  10.         if (file_exists($tmpPath))
  11.             $tmpChange=filemtime($tmpPath);
  12.         $tplChange=filemtime($Path);
  13.         if ($tplChange+1==$tmpChange) include($tmpPath.'.coll.php');
  14.         elseif ($tplChange!=$tmpChange) $needCompile=true;
  15.         if ($needCompile) {
  16.             # Вызов компилятора
  17.             include_once 'tcompiler.class.php';
  18.             $compiler = new tcompiler($this,$this->tmpDir);
  19.             $compiler->compile($this->tplDir.'/'.$Path,$tmpPath);
  20.         }        
  21.         include $tmpPath;
  22.     }
  23. }
  24. ?>

Как видите, не густо, зато быстро!
На первый взгляд может показаться что такая автозамена:
  1. $tplName='tpl_'.str_replace(array('/','\\'),'.',$path).'.php';
Работает довольно долго и лучше использовать хэш, но я протестировал, хэш работает дольше.

Сравнение кода:

Решил в удобном виде привести листинги кода в разных шаблонизаторах делающих одно и тоже, чтобы можно быро сравнить читаемость и удобство подходов
Мой
  1. $tpl->num=4815162342;
  2. $tpl->post['page']['id']=316;
  3. for ($i=1; $i<30; $i++) $tpl->bin[]=array('dec'=>$i, 'bin'=>decbin($i));
  4. for ($i=1; $i<10; $i++) for ($j=1; $j<10; $j++) $tpl->table[$i]['row'][$j]['num']=$i*$j;
Smarty/Quicky
  1. $smarty->assign("num",4815162342);
  2. $smarty->assign("post",array('page'=>array('id'=>316)));
  3. for ($i=1; $i<30; $i++) $bin[]=array('dec'=>$i, 'bin'=>decbin($i));
  4. $smarty->assign("bin",$bin);
  5. for ($i=1; $i<10; $i++) for ($j=1; $j<10; $j++) $table[$i]['row'][$j]['num']=$i*$j;
  6. $smarty->assign("table",$table);
Xtemplate
  1. $xtpl->assign('num',4815162342);
  2. $post['page']['id']=316;
  3. $xtpl->assign('post',$post);
  4. for ($i=1; $i<30; $i++) $xtpl->insert_loop("page.bin",array("dec"=>$i,"bin"=>decbin($i)));
  5. for ($i=1; $i<10; $i++) {
  6.         for ($j=1; $j<10; $j++) $xtpl->insert_loop("page.table.row",'rownum',$i*$j);
  7.         $xtpl->parse("page.table");    
  8. }

Подключение:

Подключается шаблонизатор следующим образом:
  1. require_once 'путь_до_шаблонизатоора/tpl.class.php';
  2. $tpl=new tpl('путь_к_папке_с_шаблонами','путь_к_папке_с_кешем');
Не забудьте дать права папке с кешем!

Скачать:

Пока шаблонизатор лежит вот тут скачать, пока это только Бета-версия, поэтому не стоит тестить на серьёзных проектах, я лишь хотел выслушать замечания, и идеи на эту тему!
Если эксперемент удасться и подобный гибрид нативного и обычного шаблонизатора будет кому-то нужен, обязательно, буду его развивать. Кстати скорее всего он будет называться «LL».
По поводу багов просьба отписываться на oleg<собака>emby.ru

Заключение:

В заключение не буду делать громких заявлений, вроде «В отдельных случаях данный шаблонизатор быстрее php native». Все мы понимаем, что в отдельных случаях трактор «Беларусь» может оказаться быстрее новенькой Porshe Panamera, в любом случае шаблонизатор будет медленнее, хотябы потому что ему нужно сравнивать даты изменения шаблона и его откомпилированной версии, а это два лишних обращения к ФС. Касатемо оптимизаций, никто не мешает оптимизировать и нативный код.
Разумеется, как и все шаблонизаторы, мой работает медленнее нативного php, но на самую малость, в доказательство привожу результаты тестов:
Сравнение производительности шаблонизаторов
Все тесты проводил по несколько раз, дабы убедиться, что НЛО не повлияло на результаты. Если что выложил их сюда.
Tags:
Hubs:
+3
Comments 44
Comments Comments 44

Articles