Целью этой статьи является разработка простого шаблонизатора, который смог бы удовлетворить основные потребности разработчика в моём лице, да и не только в моём. Принцип работы его будет основан на native методе, который вместе с другим детальнее описан тут для тех, кто еще не в курсе.
Итак, какой функционал я хочу? Для начала, это должно быть довольно простое решение без всяких рюшиков типа "куча хелперов". Конечно, кому-то это нужно, но у меня обычно всё проще, поэтому обойдёмся. То есть, должно быть независимое решение в виде одного класса. Сразу скажу, что кеширование результатов тоже упустим, дабы не отвлекаться от сути, да и задача эта в большинстве случаев принадлежит другим. Кому надо, легко может его прикрутить как внутри так и снаружи, используя этот материал. Далее я также хочу спокойно выполнять такие вещи как:
Удобное извлечение переменных в шаблоне
Вставка шаблона в шаблон
Корректная работа с HTML кодом
Режим отладки
Этим списком, думаю, я буду полностью удовлетворен.
Начнем с переменных. Тут, как и везде далее все просто - магический метод __get нам поможет обращаться к переменной по ее имени. Всё начинает выглядеть примерно так:
Listing №1 (PHP)
class Template {
/**
* @var array
*/
private $_Vars;
/**
* @var string
*/
private $_Name;
/**
* @var string
*/
private $_Path;
public function __construct()
{
$this->_Name = '';
$this->_Vars = array();
$this->_Path = '';
}
/**
* @param string $VarName имя переменной
* @param string $VarValue значение переменной
* @return Template
*/
public function AddVar($VarName, $VarValue)
{
if ($VarName !== '') {
$this->_Vars[$VarName] = $VarValue;
}
return $this;
}
/**
* @param string $Varname имя переменной
* @return mixed
*/
public function __get($VarName)
{
if (array_key_exists($VarName, $this->_Vars)) {
if ($this->_Vars[$VarName] instanceof Template) {
return $this->_Vars[$VarName]->Prepare();
}
else{
return $this->_Vars[$VarName];
}
}
return NULL;
}
/**
* @var string $Name имя шаблона
*/
public function SetName($Name)
{
$this->_Name = $Name;
}
/**
* @var string $Path путь к шаблону(ам)
*/
public function SetPath($Path)
{
$this->_Path = $Path;
}
}
Теперь двумя следующими методами обеспечим корректность выводимых данных. Первый заботиться об спецсимволах HTML, а второй о валидности ccылок:
Listing №2 (PHP)
/**
* @param mixed $Var
* @return mixed
*/
protected function EscapeHtml($Var)
{
if(is_array($Var)) {
foreach($Var as &$VarItem) {
$VarItem = $this->EscapeHtml($VarItem);
}
}
else {
$Var = htmlspecialchars($Var, ENT_QUOTES);
}
return $Var;
}
/**
* @param mixed $Var
* @return mixed
*/
protected function EscapeUrl($Var)
{
if(is_array($Var)) {
foreach($Var as &$VarItem) {
$VarItem = $this->EscapeUrl($Var);
}
}
else {
$Var = htmlentities($Var, ENT_QUOTES);
}
return $Var;
}
Идем дальше.
Что я подразумеваю под словами "режим отладки"? Это, скорее всего, больше похоже на режим удобного чтения. В таком режиме на выходе мы получим код шаблона в том же виде, что и до его парсинга, то есть со всеми табуляциями, переводами строк и прочей нечистью. Это довольно удобно, потому что native шаблоны могут сложно восприниматься и то и дело приходится всё табулировать и переносить. В противоположность этому режиму будет обычный рабочий режим, где ненужные символы будут удалены за ненадобностью. За это будет отвечать метод _Zip, а за переключение между режимами статический метод SetDebug:
Listing №3 (PHP)
/**
* Свойство хранящее текущий режим
* @static
* @var bool
*/
private static $_Debug = FALSE;
/**
* Вкл/выкл дебаг режим
* @static
* @param bool $TrueOrFalse TRUE
*/
public static function SetDebug($TrueOrFalse = TRUE)
{
self::$_Debug = $TrueOrFalse;
}
/**
* Возвращает $Text с удаленными "\t", "\n" и "\r"
* @param string $Text
* @return string
*/
private function _Zip($Text)
{
return (empty($Text)) ? $Text : str_replace(array("\t", "\n", "\r"), '', $Text);
}
Теперь нам нужен метод Prepare, который выполнит всю основную работу, но не выведет данные, а возвратит их строкой. Это нам понадобиться для реализации вставки шаблона в шаблон, которая и происходит при извлечении переменной в шаблоне (метод __get). Думаю, без буферизации вывода тут не обойтись:
Listing №4 (PHP)
/**
* @return string
*/
public function Prepare()
{
if (file_exists($this->_Path . $this->_Name)) {
ob_start();
${__CLASS__} = $this;
include $this->_Path . $this->_Name;
unset(${__CLASS__});
return (self::$_Debug) ? ob_get_clean() : $this->_Zip(ob_get_clean());
}
else {
throw new Exception('Template file "' . $this->_Path . $this->_Name . '" does not exists');
}
}
Сначала проверяем наличие шаблона. Затем "по родному" парсим шаблон и возвращаем контент из буфера, очистив ненужные символы, если это необходимо. Также, меня очень напрягает писать в шаблонах $this, поэтому я решил писать там $Template. Мне так очень нравится.
Наконец, добавив метод непосредственного вывода, получаем нужный "полный фарш":
Listing №5 (PHP)
class Template {
/**
* @var string
*/
private $_Name;
/**
* @var array
*/
private $_Vars;
/**
* @var string
*/
private $_Path;
/**
* @static
* @var bool
*/
private static $_Debug = FALSE;
/**
* @static
* @param bool $TrueOrFalse TRUE
*/
public static function SetDebug($TrueOrFalse = TRUE)
{
self::$_Debug = $TrueOrFalse;
}
public function __construct()
{
$this->_Name = '';
$this->_Vars = array();
$this->_Path = '';
}
/**
* @param string $VarName
* @param string $VarValue
* @return Template
*/
public function AddVar($VarName, $VarValue)
{
if ($VarName !== '') {
$this->_Vars[$VarName] = $VarValue;
}
return $this;
}
/**
* @param string $Varname
* @return mixed
*/
public function __get($VarName)
{
if (array_key_exists($VarName, $this->_Vars)) {
if ($this->_Vars[$VarName] instanceof Template) {
return $this->_Vars[$VarName]->Prepare();
}
else{
return $this->_Vars[$VarName];
}
}
return NULL;
}
/**
* @var string $Name
*/
public function SetName($Name)
{
$this->_Name = $Name;
}
/**
* @var string $Path
*/
public function SetPath($Path)
{
$this->_Path = $Path;
}
/**
* @return string
*/
public function Prepare()
{
if (file_exists($this->_Path . $this->_Name)) {
ob_start();
${__CLASS__} = $this;
include $this->_Path . $this->_Name;
unset(${__CLASS__});
return (self::$_Debug) ? ob_get_clean() : $this->_Zip(ob_get_clean());
}
else {
throw new Exception('Template file "' . $this->_Path . $this->_Name . '" does not exists');
}
}
public function Display()
{
print ($this->Prepare());
}
/**
* @param mixed $Var
* @return mixed
*/
protected function EscapeHtml($Var)
{
if(is_array($Var)) {
foreach($Var as &$VarItem) {
$VarItem = $this->EscapeHtml($VarItem);
}
}
else {
$Var = htmlspecialchars($Var, ENT_QUOTES);
}
return $Var;
}
/**
* @param mixed $Var
* @return mixed
*/
protected function EscapeUrl($Var)
{
if(is_array($Var)) {
foreach($Var as &$VarItem) {
$VarItem = $this->EscapeUrl($Var);
}
}
else {
$Var = htmlentities($Var, ENT_QUOTES);
}
return $Var;
}
/**
* @param string $Text
* @return string
*/
private function _Zip($Text)
{
return (empty($Text)) ? $Text : str_replace(array("\t", "\n", "\r"), '', $Text);
}
}
Ну и напоследок, небольшой пример использования.
Допустим, у нас есть два шаблона - главная страница page.php и список пользователей userslist.php. Нужно отобразить этот список вместе с информацией на главной странице. Поскупившись на контент, рисуем главную:
Listing №6 (PHP)
<div style="border: 1px solid #E0E3F3;">
<?php echo $Template->UserList;?>
</div>
<a href="<?php echo $Template->EscapeUrl($Template->Link)?>">About</a>
А также список пользователей:
Listing №7 (PHP)
<div>
<?php foreach($Template->EscapeHtml($Template->UserNames) as $UserName) { ?>
<span style="color: #FF0000;"><?php echo $UserName;?></span><br>
<?php } ?>
</div>
За логику отвечает некая модель:
Listing №8 (PHP)
//Подключаем шаблонизатор
require 'Template.php';
//Устанавливаем режим отладки включенным для всех шаблонов
Template::SetDebug(TRUE);
//Инициализируем подшаблон списка пользователей
$UsersListView = new Template();
$UsersListView->SetName('userslist.html');
//Список пользователей
$UserNames = array('Den Rights©', 'Dasha Nasha', 'Samuil Kakojto');
$UsersListView->AddVar('UserNames', $UserNames);
//Инициализируем главный шаблон страницы
$PageView = new Template();
$PageView->SetName('page.html');
$PageView->AddVar('UserList', $UsersListView);
$PageView->AddVar('Link', 'http://www.itdumka.com.ua/index.php?cmd=shownode&node=1');
$PageView->Display();
В результате работы с включенным режимом отладки, в отличие от обычного режима, где все будет в одну строку, мы увидим такую картину:
Listing №9 (HTML)
<div style="border: 1px solid #E0E3F3;">
<div>
<span style="color: #FF0000;">Den Rights©</span><br>
<span style="color: #FF0000;">Dasha Nasha</span><br>
<span style="color: #FF0000;">Samuil Kakojto</span><br>
</div>
</div>
<a href="http://www.itdumka.com.ua/index.php?cmd=shownode&node=1">About</a>
Вот так.