ITDumka
Мой родной PHP шаблонизатор

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

Итак, какой функционал я хочу? Для начала, это должно быть довольно простое решение без всяких рюшиков типа "куча хелперов". Конечно, кому-то это нужно, но у меня обычно всё проще, поэтому обойдёмся. То есть, должно быть независимое решение в виде одного класса. Сразу скажу, что кеширование результатов тоже упустим, дабы не отвлекаться от сути, да и задача эта в большинстве случаев принадлежит другим. Кому надо, легко может его прикрутить как внутри так и снаружи, используя этот материал. Далее я также хочу спокойно выполнять такие вещи как:

  • Удобное извлечение переменных в шаблоне
  • Вставка шаблона в шаблон
  • Корректная работа с HTML кодом
  • Режим отладки

Этим списком, думаю, я буду полностью удовлетворен.

Начнем с переменных. Тут, как и везде далее все просто - магический метод __get нам поможет обращаться к переменной по ее имени. Всё начинает выглядеть примерно так:


Listing №1 (PHP)
  1. class Template {
  2.   /**
  3.    * @var array
  4.    */
  5.   private $_Vars;
  6.   /**
  7.    * @var string
  8.    */
  9.   private $_Name;
  10.   /**
  11.    * @var string
  12.    */
  13.   private $_Path;
  14.  
  15.   public function __construct()
  16.   {
  17.     $this->_Name = '';
  18.     $this->_Vars = array();
  19.     $this->_Path = '';
  20.   }
  21.   /**
  22.    * @param string $VarName имя переменной
  23.    * @param string $VarValue значение переменной
  24.    * @return Template
  25.    */
  26.   public function AddVar($VarName, $VarValue)
  27.   {
  28.     if ($VarName !== '') {
  29.       $this->_Vars[$VarName] = $VarValue;
  30.     }
  31.     return $this;
  32.   }
  33.   /**
  34.    * @param string $Varname имя переменной
  35.    * @return mixed
  36.    */
  37.   public function __get($VarName)
  38.   {
  39.     if (array_key_exists($VarName, $this->_Vars)) {
  40.       if ($this->_Vars[$VarName] instanceof Template) {
  41.         return $this->_Vars[$VarName]->Prepare();
  42.       }
  43.       else{
  44.         return $this->_Vars[$VarName];
  45.       }
  46.     }
  47.     return NULL;
  48.   }
  49.   /**
  50.    * @var string $Name имя шаблона
  51.    */
  52.   public function SetName($Name)
  53.   {
  54.     $this->_Name = $Name;
  55.   }
  56.   /**
  57.    * @var string $Path путь к шаблону(ам)
  58.    */
  59.   public function SetPath($Path)
  60.   {
  61.     $this->_Path = $Path;
  62.   }
  63. }


Теперь двумя следующими методами обеспечим корректность выводимых данных. Первый заботиться об спецсимволах HTML, а второй о валидности ccылок:


Listing №2 (PHP)
  1.   /**
  2.    * @param mixed $Var
  3.    * @return mixed
  4.    */
  5.   protected function EscapeHtml($Var)
  6.   {
  7.     if(is_array($Var)) {
  8.       foreach($Var as &$VarItem) {
  9.         $VarItem = $this->EscapeHtml($VarItem);
  10.       }
  11.     }
  12.     else {
  13.       $Var = htmlspecialchars($Var, ENT_QUOTES);
  14.     }
  15.     return $Var;
  16.   }
  17.   /**
  18.    * @param mixed $Var
  19.    * @return mixed
  20.    */
  21.   protected function EscapeUrl($Var)
  22.   {
  23.     if(is_array($Var)) {
  24.       foreach($Var as &$VarItem) {
  25.         $VarItem = $this->EscapeUrl($Var);
  26.       }
  27.     }
  28.     else {
  29.       $Var = htmlentities($Var, ENT_QUOTES);
  30.     }
  31.     return $Var;
  32.   }


Идем дальше.

Что я подразумеваю под словами "режим отладки"? Это, скорее всего, больше похоже на режим удобного чтения. В таком режиме на выходе мы получим код шаблона в том же виде, что и до его парсинга, то есть со всеми табуляциями, переводами строк и прочей нечистью. Это довольно удобно, потому что native шаблоны могут сложно восприниматься и то и дело приходится всё табулировать и переносить. В противоположность этому режиму будет обычный рабочий режим, где ненужные символы будут удалены за ненадобностью. За это будет отвечать метод _Zip, а за переключение между режимами статический метод SetDebug:


Listing №3 (PHP)
  1.   /**
  2.    * Свойство хранящее текущий режим
  3.    * @static
  4.    * @var bool
  5.    */
  6.   private static $_Debug = FALSE;
  7.   /**
  8.    * Вкл/выкл дебаг режим
  9.    * @static
  10.    * @param bool $TrueOrFalse TRUE
  11.    */
  12.   public static function SetDebug($TrueOrFalse = TRUE)
  13.   {
  14.     self::$_Debug = $TrueOrFalse;
  15.   }
  16.   /**
  17.    * Возвращает $Text с удаленными "\t", "\n" и "\r"
  18.    * @param  string $Text
  19.    * @return string
  20.    */
  21.   private function _Zip($Text)
  22.   {
  23.     return (empty($Text)) ? $Text : str_replace(array("\t", "\n", "\r"), '', $Text);
  24.   }


Теперь нам нужен метод Prepare, который выполнит всю основную работу, но не выведет данные, а возвратит их строкой. Это нам понадобиться для реализации вставки шаблона в шаблон, которая и происходит при извлечении переменной в шаблоне (метод __get). Думаю, без буферизации вывода тут не обойтись:


Listing №4 (PHP)
  1.   /**
  2.    * @return string
  3.    */
  4.   public function Prepare()
  5.   {
  6.     if (file_exists($this->_Path . $this->_Name)) {
  7.       ob_start();
  8.       ${__CLASS__} = $this;
  9.       include $this->_Path . $this->_Name;
  10.       unset(${__CLASS__});
  11.       return (self::$_Debug) ? ob_get_clean() : $this->_Zip(ob_get_clean());
  12.     }
  13.     else {
  14.       throw new Exception('Template file "' . $this->_Path . $this->_Name . '" does not exists');
  15.     }
  16.   }


Сначала проверяем наличие шаблона. Затем "по родному" парсим шаблон и возвращаем контент из буфера, очистив ненужные символы, если это необходимо. Также, меня очень напрягает писать в шаблонах $this, поэтому я решил писать там $Template. Мне так очень нравится.

Наконец, добавив метод непосредственного вывода, получаем нужный "полный фарш":


Listing №5 (PHP)
  1. class Template {
  2.   /**
  3.    * @var string
  4.    */
  5.   private $_Name;
  6.   /**
  7.    * @var array
  8.    */
  9.   private $_Vars;
  10.   /**
  11.    * @var string
  12.    */
  13.   private $_Path;
  14.   /**
  15.    * @static
  16.    * @var bool
  17.    */
  18.   private static $_Debug = FALSE;
  19.   /**
  20.    * @static
  21.    * @param bool $TrueOrFalse TRUE
  22.    */
  23.   public static function SetDebug($TrueOrFalse = TRUE)
  24.   {
  25.     self::$_Debug = $TrueOrFalse;
  26.   }
  27.  
  28.   public function __construct()
  29.   {
  30.     $this->_Name = '';
  31.     $this->_Vars = array();
  32.     $this->_Path = '';
  33.   }
  34.   /**
  35.    * @param string $VarName
  36.    * @param string $VarValue
  37.    * @return Template
  38.    */
  39.   public function AddVar($VarName, $VarValue)
  40.   {
  41.     if ($VarName !== '') {
  42.       $this->_Vars[$VarName] = $VarValue;
  43.     }
  44.     return $this;
  45.   }
  46.   /**
  47.    * @param string $Varname
  48.    * @return mixed
  49.    */
  50.   public function __get($VarName)
  51.   {
  52.     if (array_key_exists($VarName, $this->_Vars)) {
  53.       if ($this->_Vars[$VarName] instanceof Template) {
  54.         return $this->_Vars[$VarName]->Prepare();
  55.       }
  56.       else{
  57.         return $this->_Vars[$VarName];
  58.       }
  59.     }
  60.     return NULL;
  61.   }
  62.   /**
  63.    * @var string $Name
  64.    */
  65.   public function SetName($Name)
  66.   {
  67.     $this->_Name = $Name;
  68.   }
  69.   /**
  70.    * @var string $Path
  71.    */
  72.   public function SetPath($Path)
  73.   {
  74.     $this->_Path = $Path;
  75.   }
  76.   /**
  77.    * @return string
  78.    */
  79.   public function Prepare()
  80.   {
  81.     if (file_exists($this->_Path . $this->_Name)) {
  82.       ob_start();
  83.       ${__CLASS__} = $this;
  84.       include $this->_Path . $this->_Name;
  85.       unset(${__CLASS__});
  86.       return (self::$_Debug) ? ob_get_clean() : $this->_Zip(ob_get_clean());
  87.     }
  88.     else {
  89.       throw new Exception('Template file "' . $this->_Path . $this->_Name . '" does not exists');
  90.     }
  91.   }
  92.  
  93.   public function Display()
  94.   {
  95.     print ($this->Prepare());
  96.   }
  97.   /**
  98.    * @param mixed $Var
  99.    * @return mixed
  100.    */
  101.   protected function EscapeHtml($Var)
  102.   {
  103.     if(is_array($Var)) {
  104.       foreach($Var as &$VarItem) {
  105.         $VarItem = $this->EscapeHtml($VarItem);
  106.       }
  107.     }
  108.     else {
  109.       $Var = htmlspecialchars($Var, ENT_QUOTES);
  110.     }
  111.     return $Var;
  112.   }
  113.   /**
  114.    * @param mixed $Var
  115.    * @return mixed
  116.    */
  117.   protected function EscapeUrl($Var)
  118.   {
  119.     if(is_array($Var)) {
  120.       foreach($Var as &$VarItem) {
  121.         $VarItem = $this->EscapeUrl($Var);
  122.       }
  123.     }
  124.     else {
  125.       $Var = htmlentities($Var, ENT_QUOTES);
  126.     }
  127.     return $Var;
  128.   }
  129.   /**
  130.    * @param  string $Text
  131.    * @return string
  132.    */
  133.   private function _Zip($Text)
  134.   {
  135.     return (empty($Text)) ? $Text : str_replace(array("\t", "\n", "\r"), '', $Text);
  136.   }
  137. }


Ну и напоследок, небольшой пример использования.

Допустим, у нас есть два шаблона - главная страница page.php и список пользователей userslist.php. Нужно отобразить этот список вместе с информацией на главной странице. Поскупившись на контент, рисуем главную:


Listing №6 (PHP)
  1. <div style="border: 1px solid #E0E3F3;">
  2.   <?php echo $Template->UserList;?>
  3. </div>
  4. <a href="<?php echo $Template->EscapeUrl($Template->Link)?>">About</a>


А также список пользователей:


Listing №7 (PHP)
  1. <div>
  2.   <?php foreach($Template->EscapeHtml($Template->UserNames) as $UserName) { ?>
  3.   <span style="color: #FF0000;"><?php echo $UserName;?></span><br>
  4.   <?php } ?>
  5. </div>


За логику отвечает некая модель:


Listing №8 (PHP)
  1. //Подключаем шаблонизатор
  2. require 'Template.php';
  3. //Устанавливаем режим отладки включенным для всех шаблонов
  4. Template::SetDebug(TRUE);
  5. //Инициализируем подшаблон списка пользователей
  6. $UsersListView = new Template();
  7. $UsersListView->SetName('userslist.html');
  8. //Список пользователей
  9. $UserNames = array('Den Rights©', 'Dasha Nasha', 'Samuil Kakojto');
  10. $UsersListView->AddVar('UserNames', $UserNames);
  11. //Инициализируем главный шаблон страницы
  12. $PageView = new Template();
  13. $PageView->SetName('page.html');
  14. $PageView->AddVar('UserList', $UsersListView);
  15. $PageView->AddVar('Link', 'http://www.itdumka.com.ua/index.php?cmd=shownode&node=1');
  16. $PageView->Display();
  17.  


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


Listing №9 (HTML)
  1. <div style="border: 1px solid #E0E3F3;">
  2.   <div>
  3.     <span style="color: #FF0000;">Den Rights©</span><br>
  4.     <span style="color: #FF0000;">Dasha Nasha</span><br>
  5.     <span style="color: #FF0000;">Samuil Kakojto</span><br>
  6.   </div>
  7. </div>
  8. <a href="http://www.itdumka.com.ua/index.php?cmd=shownode&node=1">About</a>


Вот так.

Comments
By   Kuroki Kaze
Published   16.10.2009

А какой смысл делать в AddVar

Listing №1 ()
  1. return $this
? Chained methods то всё равно не работают.

Published   16.10.2009

Listing №1 (PHP)
  1. $PageView->AddVar('UserList', $UsersListView)
  2. ->AddVar('Link', 'http://www.itdumka.com.ua/index.php?cmd=shownode&node=1');

Работает

By   Rpsl
Published   17.10.2009

Не плохо, лучше чем на двухстраничный сайт подключать смарти.


Хочу вам порекомендовать в шаблонах использовать более простые конструкции:

Listing №1 (PHP)
  1.  
  2. <div>
  3.   <?php foreach($Template->EscapeHtml($Template->UserNames) as $UserName) { ?>
  4.   <span style="color: #FF0000;"><?php echo $UserName;?></span><br>
  5.   <?php } ?>
  6. </div>
  7.  


Проще и красивее записать как:

Listing №2 (PHP)
  1.  
  2. <div>
  3.   <? foreach($Template->EscapeHtml($Template->UserNames) as $UserName): ?>
  4.   <span style="color: #FF0000;"><?=$UserName;?></span><br>
  5.   <? endforeach;?>
  6. </div>
  7.  


Published   17.10.2009

Rpsl
Хочу вам порекомендовать в шаблонах использовать более простые конструкции

Да я в принципе так и делаю, но вот с короткими тегами не соглашусь.

By   Дмитрий
Published   23.10.2009

Если в методе prepare() перед инклюдом шаблона написать:

Listing №1 (unknown)
  1.  
  2. if(count($this->_Vars))
  3.     foreach($this->_Vars as $k=>$v)
  4.         $$k=$v; //двойной знак $!!!
  5.  
, то в шаблоне можно будет использовать переменные, просто обращаясь к ним по имени. Например, $UserNames, а не $Templates->UserNames


Published   23.10.2009

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

Listing №1 (PHP)
  1. extract($this->_Vars)

By   Amen
Published   14.12.2009

Вот скажи мне зачем для этого дела использовать ООП? Разве нельзя было обойтись функциями?

Published   14.12.2009

Amen
Вот скажи мне зачем для этого дела использовать ООП? Разве нельзя было обойтись функциями?

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

By   Silicium
Published   14.01.2010

Интересует режим отладки.


Зачем он?


Лучше когда он всегда включен, это же приятно читать красивый html-код, а не все в одну строчку.

Published   15.01.2010

Silicium
Зачем он?

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

By   Дима
Published   21.01.2010

Что вы подразумеваете под функциональным программированием? не процедурный ли стиль случайно?


И ООП - это не обязательно классы.

Published   21.01.2010

Имеется в виду процедурный стиль

Дима
И ООП - это не обязательно классы.

А что это еще?

By   хуец <- Как мило ты себя назвал ГЫ
Published   12.02.2010

Друпал - ООП-система. Хотя там все на ф-циях/процедурах сделано

Published   12.02.2010

ну вот это "хотя" немного вносит неясность, не так ли?

By   Volt(220)
Published   13.02.2010

Я так понимаю, что вот это:

Listing №1 (PHP)
  1. if ($this->_Vars[$VarName] instanceof Template) {
  2.         return $this->_Vars[$VarName]->Prepare();

нужно для обработки подшаблонов.

Не лучше ли оставить для __get одну функцию - возврат переменной?

И тут возникает море идей =)

1)Воспользоваться магическим __toString() типа такого:

Listing №2 (PHP)
  1. public function __toString(){
  2.     return $this->Prepare();
  3. }

Развивая идею можно отказаться от методов Display и Prepare.

2) Оставить один Prepare и делать вывод подшаблона:

Listing №3 (PHP)
  1. echo $Template->$SubTemplate->Prepare();

3) Оставить как есть и делать вывод подшаблона:

Listing №4 (PHP)
  1. $Template->$SubTemplate->Display();


Published   13.02.2010

Volt(220)
Развивая идею можно отказаться от методов Display и Prepare.

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

By   даяавтор
Published   06.03.2010

Прусь с этого программиста,мало того что человек про extract ни слухом,ни духом,так еще и цикл юзать чтобы функцией массив обработать.Быдлокод захватит мир.


Published   06.03.2010

даяавтор

Это вы про какого программиста?

By   Man
Published   07.07.2010

неплохо


Долго к этому приходил? Что почитать посоветуешь?


Вроде стандартный набор шаблонизатора... неплохо.



Published   07.07.2010

Man
Долго к этому приходил? Что почитать посоветуешь?

А что тут идти, я уже давно это прошел ;)

Советовать? Не знаю, смотря чего ты хочешь. Лучше всего - практика, а в книжках всё одинаковое и сырое.

Create comment
 
Formatting
Comment can not be edited. Please, use the button "Preview"
By
  (Enter prev char)
Comment
Categories
PHP
12
articles
Прочее
4
articles
Delphi
0
article
C/C++
0
article
C#
0
article
Java
0
article
Perl
0
article
Python
0
article
Enter
Cookie must be "ON"
Login
Password
 
Popular tags
PHP
9
articles
паттерн
5
articles
framework
5
articles
шаблон
5
articles
Template View
3
articles
Facade
3
articles
Service Stub
3
articles
Page Controller
2
articles
Singleton
2
articles
Gateway
2
articles
MySQL
2
articles
Registry
2
articles
Command
2
articles
Front Controller
2
articles
Action
2
articles
Abstract Factory
2
articles
типовое решение
2
articles
шаблоны проектирования
2
articles
Iterator
2
articles
Transaction Script
2
articles
Rambler's Top100 Правильный CSS!