22 November 2008

Нативный шаблонизатор

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

  1. $title = 'My title';
  2. include('templates/index.html');
* This source code was highlighted with Source Code Highlighter.

  1. <html><head><title><?php echo $title ?></title></head>
  2. <!-- ... -->
* This source code was highlighted with Source Code Highlighter.


То есть, переменную определили и приинклюдили html-файл. Я считаю, что это в корне неверный подход. Почему?

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

Таким образом, я пришел к выводу, что шаблонизатор нужен, но он не должен быть навороченным тормозом типа Smarty.
Идеология блочных шаблонизаторов (XTemplate, например) мне не импонирует потому, что в них нет ветвлений как таковых, есть только циклы.

Потому я написал свой.

Начнем с того, что нам нужно разобраться с обработкой ошибок. Я использую для этой цели исключения, потому определяем класс исключений:

  1. class STempException extends Exception {}
* This source code was highlighted with Source Code Highlighter.

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

  1. class STemp
  2. {
  3.   /**
  4.    * The name of the directory where templates are located.
  5.    *
  6.    * @var string.
  7.    * @access private.
  8.    */
  9.   private $path;
  10.   
  11.   /**
  12.    * Name of the template.
  13.    *
  14.    * @var string.
  15.    * @access private.
  16.    */
  17.   private $template;
  18.   
  19.   /**
  20.    * Where assigned template vars are kept.
  21.    *
  22.    * @var array.
  23.    * @access private.
  24.    */
  25.   private $variables = array();
  26.   
  27.   /**
  28.    * Parameters of the template engine.
  29.    *
  30.    * @var array.
  31.    * @access private
  32.    */
  33.   private $params = array(
  34.     'xss_protection' => true,
  35.     'exit_after_display' => true,
  36.     'endofline_to_br' => false
  37.     );
  38.   
  39.   /**
  40.    * File that include in template.
  41.    *
  42.    * @var string.
  43.    * @access private.
  44.    */
  45.   private $include_file;
  46.   
  47.   /**
  48.    * The class constructor. Set name of the directory where templates are located.
  49.    *
  50.    * @param string $path name of the directory where templates are located, default 'templates/'.
  51.    * @access public.
  52.    */
  53.   public function __construct($path = 'templates/')
  54.   {
  55.     $this->path = $path;
  56.   }
  57.   
  58.   /**
  59.    * Set parameters of template engine.
  60.    *
  61.    * @param string $param name of the parameter.
  62.    * @param bool $value value of the parameter.
  63.    * @return bool TRUE if parameter set, FALSE if didn't set.
  64.    * @access public.
  65.    */
  66.   public function setParam($param, $value)
  67.   {
  68.     if (isset($this->params[$param])) {
  69.       $this->params[$param] = $value;
  70.       return true;
  71.     }
  72.       
  73.     return false;
  74.   }
  75.   
  76.   /**
  77.    *
  78.    * @param string $include_file path to include file.
  79.    * @access public.
  80.    */
  81.   public function setIncludeFile($include_file)
  82.   {          
  83.     $this->include_file = $this->path.$include_file;
  84.     
  85.     if (!file_exists($this->path.$include_file))
  86.       throw new STempException('Include file '.$this->include_file.' not exitst');
  87.   }
  88.   
  89.   /**
  90.    * Assigns values to template variables.
  91.    *
  92.    * @param string $name the template variable name.
  93.    * @param mixed $value the value to assign.
  94.    * @access public.
  95.    */
  96.   public function assign($name, $value)
  97.   {
  98.     $this->variables[$name] = $value;
  99.   }
  100.   
  101.   /**
  102.    * Executes and displays the template results.
  103.    *
  104.    * @param string $template the template name.
  105.    * @access public.
  106.    */
  107.   public function display($template)
  108.   {
  109.     $this->template = $this->path.$template;
  110.     
  111.     if (!file_exists($this->template))
  112.       throw new STempException('Template file '.$template.' not exitst');
  113.       
  114.     require_once($this->template);
  115.     
  116.     if ($this->params['exit_after_display'])
  117.       exit;
  118.   }
  119.   
  120.   /**
  121.    * Get value of template variable.
  122.    *
  123.    * @param string $name the template variable name.
  124.    * @return mixed value of template variable with this name. FALSE if variable not set.
  125.    * @access private.
  126.    */
  127.   private function __get($name)
  128.   {
  129.     if (isset($this->variables[$name])) {
  130.       $variable = $this->variables[$name];
  131.       
  132.       if ($this->params['xss_protection'])
  133.         $variable = $this->xssProtection($variable);
  134.         
  135.       if ($this->params['endofline_to_br'])
  136.         $variable = $this->endoflineToBr($variable);
  137.         
  138.       return $variable;
  139.     }
  140.     
  141.     return NULL;
  142.   }
  143.   
  144.   /**
  145.    * Include file
  146.    *
  147.    * @access private
  148.    */
  149.   private function includeFile()
  150.   {
  151.     if (!file_exists($this->include_file))
  152.       throw new STempException('Include file '.$this->include_file.' not found');
  153.       
  154.     require_once($this->include_file);
  155.   }
  156.   
  157.   /**
  158.    * For the formation of endings of words.
  159.    *
  160.    * @param int $value number.
  161.    * @param string $word0 word in the singular.
  162.    * @param string $word1 word in the plural (2, 3).
  163.    * @param string $word2 word in the plural.
  164.    * @param string $separator separator, default ' '.
  165.    * @return string formed words
  166.    * @access private.
  167.    */
  168.   private function morph($value, $word0, $word1, $word2, $separator = ' ')
  169.   {
  170.     if (preg_match('/1\d$/', $value))
  171.       return $value.$separator.$word2;
  172.     elseif (preg_match('/1$/', $value))
  173.       return $value.$separator.$word0;
  174.     elseif (preg_match('/(2|3|4)$/', $value))
  175.       return $value.$separator.$word1;
  176.     else
  177.       return $value.$separator.$word2;
  178.   }
  179.   
  180.   /**
  181.    * For protection from XSS.
  182.    *
  183.    * @param mixed $variable data for protection.
  184.    * @return mixed protected data.
  185.    * @access private.
  186.    */
  187.   private function xssProtection($variable)
  188.   {
  189.     if (is_array($variable)) {
  190.       $protected = array();
  191.       foreach ($variable as $key=>$value)
  192.         $protected[$key] = $this->xssProtection($value);
  193.       return $protected;
  194.     }
  195.     
  196.     return htmlspecialchars($variable);
  197.   }
  198.   
  199.   /**
  200.    * Inserts HTML line breaks before all newlines in a string.
  201.    *
  202.    * @param mixed $variable data for protection.
  203.    * @return mixed data where string with <br /> inserted before all newlines.
  204.    * @access private.
  205.    */
  206.   private function endoflineToBr($variable)
  207.   {
  208.     if (is_array($variable)) {
  209.       $protected = array();
  210.       foreach ($variable as $key=>$value)
  211.         $protected[$key] = $this->endoflineToBr($value);
  212.       return $protected;
  213.     }
  214.     
  215.     return nl2br($variable);
  216.   }
  217. }
* This source code was highlighted with Source Code Highlighter.


В конструкторе мы можем указать путь к директории шаблонов (по умолчанию temlates/).

С помощью метода setParam мы можем установить параметры шаблонизатора. Их всего три (мне этого достаточно, при необходимости можно добавлять параметры). Первый параметр — xss_protection — как понятно из названия, нужен для защиты от уязвимости xss. Если значение параметра установлено как true, все переменные, которые мы используем в шаблоне, перед отдачей автоматически обрабатываются функцией htmlspecialchars (в том числе элементы массивов). Второй параметр — exit_after_display — нужен для того, чтобы, при потребности, мы могли остановить выполнение сценария после отображения шаблона. Третий параметр — endofline_to_br — обрабатывает все переменные перед отдачей (в том числе элементы массивов) функцией nl2br.

Методом setIncludeFile мы можем установить подключаемый шаблон. Очень часто используется общий шаблон index.tpl.php и в него, в зависимости от условий, подключают изменямую часть. Вот для автоматизации данного процесса и нужен этот метод. Если подключаемый файл не существует, выбрасыватся исключение.

Метод assign служит для передачи переменных в шаблон.

Метод display отображает шаблон. Если файл шаблона не существует, выбрасывается исключение. Если параметр exit_after_display установлен как true, этот метод также завершает работу сценария (практически всегда отображение шаблона является последним действием).

«Магический» метод __get возвращает значение переданной в шаблон переменной. Если переменная не определена, возвращает NULL. В зависимости от параметров, переменные перед отдачей могут обрабатываться.

Метод includeFile инклюдит файл, назначенный методом setIncludeFile и выбрасывает исключение, если этот файл не найден.

Метод morph, не совсем «шаблонизаторный», служит для формирования правильных окончание слов, относящихся к числительным. То есть, 1 комментарий, 2 комментария, 5 комментариев. В метод нужно передать само число, три разных варианта и, опционально, разделитель слов (по умолчанию неразрывный пробел).

Метод xssProtection обрабатывает данные функцией htmlspecialchars. Если на входе массив, то он рекурсивно перебирается и обрабатываются все его элементы.

Метод endoflineToBr обрабатывает данные функцией nl2br. Если на входе массив, то он, как и в предыдущем методе, рекурсивно перебирается и обрабатываются все его элементы.

Как это выглядит на практике? Предположим, нам нужно распечатать статью и комментарии к ней. Данные по статье в массиве $article, комменты — в $comments.

Контроллер:
  1. $stemp = new STemp();
  2.  
  3. $stemp->assign("title", $article['title']);
  4.  
  5. $stemp->assign("article", $article);
  6. $stemp->assign("comments", $comments);
  7.  
  8. try {
  9.   $stemp->setIncludeFile("article.tpl.php");
  10.   $stemp->display("index.tpl.php");
  11. } catch (STempException $e) {
  12.   die('STemp error: '.$e->getMessage());
  13. }
* This source code was highlighted with Source Code Highlighter.


Шаблон index.tpl.php:
  1. <html>
  2. <head>
  3. <title><?php echo $this->title ?></title>
  4. </head>
  5. <body>
  6. <?php $this->includeFile() ?>
  7. </body>
  8. </html>
* This source code was highlighted with Source Code Highlighter.


Шаблон article.tpl.php:
  1. <h1><?php echo $this->article['title'] ?></h1>
  2. <?php $this->setParam('xss_protection', false); $this->setParam('endofline_to_br', true) ?>
  3. <div class="content">
  4. <?php echo $this->article['content'] ?>
  5. </div>
  6. <p><?php echo $this->morph(count($this->comments), 'комментарий', 'комментария', 'комментариев') ?>:</p>
  7. <?php $this->setParam('xss_protecttion', true) ?>
  8. <?php foreach ($this->comments as $key=>$value) { ?>
  9. <p class="user"><?php echo $value['username'] ?>:</p>
  10. <p class="comment"><?php echo $value['text'] ?></p>
  11. <?php } ?>
* This source code was highlighted with Source Code Highlighter.


Скачать класс.

Использование класса в личных нуждах разрешено без ограничений. При перепечатке статьи или исходного кода, в том числе, частично, ссылка на меня (на мой сайт) обязательна.
Tags: php шаблонизатор нативные шаблоны
Hubs: PHP
+13
7.1k 80
Comments 86
Ads