Статьи

Помимо механизма шаблонов

В общем, движки шаблонов — это «хорошая вещь».

Я говорю это как давний программист PHP / Perl, пользователь многих шаблонизаторов (fastTemplate, Smarty, Perl’s HTML :: Template) и как автор моего собственного, bTemplate .

Однако, после долгих обсуждений с коллегой, я решил, что подавляющее большинство шаблонизаторов (включая мой собственный) просто ошибаются. Я думаю, что единственным исключением из этого правила будет Smarty , хотя я думаю, что оно просто слишком большое, и, учитывая остальную часть этой статьи, довольно бессмысленно. Однако есть несколько причин, по которым вы можете выбрать Smarty (или аналогичное решение), которые будут рассмотрены позже в этой статье.

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

Скачать и лицензирование

Класс шаблона и все примеры, используемые в этой статье, можно скачать здесь: template.zip . Вы можете использовать код в этих файлах в соответствии с MIT Open Source License, опубликованной в OSI .

Немного о шаблонах двигателей

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

  1. Как добиться этого разделения
  2. Как отделить «сложный» php-код от HTML

Теоретически это позволяет разработчикам HTML, не имеющим опыта PHP, изменять внешний вид сайта, не обращая внимания на какой-либо код PHP.

Тем не менее, системы шаблонов также внесли некоторые сложности. Во-первых, теперь у нас есть одна «страница», построенная из нескольких файлов. Как правило, у вас может быть главная страница PHP, отвечающая за бизнес-логику, внешний шаблон «макета», который отображает общий макет сайта, внутренний шаблон для конкретного содержимого, уровень абстракции базы данных и сам механизм шаблонов (который может или не может состоять из нескольких файлов). Кроме того, некоторые люди просто включают файлы «header» и «footer» в начале и конце каждой страницы PHP.

Это невероятное количество файлов для генерации одной страницы. Тем не менее, поскольку синтаксический анализатор PHP работает довольно быстро, количество используемых файлов, вероятно, не имеет значения, если ваш сайт не получает безумные объемы трафика.
Однако имейте в виду, что системы шаблонов вводят еще один уровень обработки. Файлы шаблонов нужно не только включать, но и анализировать (в зависимости от системы шаблонов это может происходить разными способами — с использованием регулярных выражений, str_replaces, компиляции, лексического анализа и т. Д.). Вот почему бенчмаркинг шаблонов стал популярным: потому что механизмы синтаксического анализа используют множество различных методов для анализа данных, некоторые из которых работают быстрее других (кроме того, некоторые механизмы шаблонов предлагают больше функций, чем другие).

Основы шаблонизатора

В основном, движки шаблонов используют язык сценариев (PHP), написанный на C. Внутри этого встроенного языка сценариев у вас есть другой язык псевдокриптов (независимо от того, какие теги поддерживает ваш механизм шаблонов). Некоторые предлагают простую переменную интерполяцию и циклы. Другие предлагают условные и вложенные циклы. Третьи (по крайней мере, Smarty) предлагают интерфейс к большому подмножеству PHP, а также кеширующий слой.

Почему я думаю, что Smarty ближе всего к праву? Потому что целью Smarty является «отделение бизнес-логики от представления», а не «отделение кода PHP от кода HTML». Хотя это кажется небольшим отличием, оно очень важно. Конечной целью любого шаблонизатора не должно быть удаление всей логики из HTML. Следует отделить логику представления от бизнес-логики.

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

Хотя Smarty делает это правильно в этом смысле (позволяя вам использовать практически все аспекты PHP), все же есть некоторые проблемы. По сути, он просто предоставляет интерфейс к PHP с новым синтаксисом. Говоря так, это кажется глупым. На самом деле проще написать {foreach --args} чем <? foreach --args ?> <? foreach --args ?> ? Если вы думаете, что это проще, спросите себя, настолько ли проще, что вы можете увидеть реальную ценность, включая огромную библиотеку шаблонов для достижения такого разделения? Конечно, Smarty предлагает много других замечательных функций, но похоже, что те же преимущества можно получить без огромных накладных расходов, связанных с включением библиотек классов Smarty.

Альтернативное решение

Решение, которое я в основном защищаю, — это «движок шаблонов», который использует PHP-код в качестве родного языка сценариев. Я знаю, что это было сделано раньше. И когда я впервые прочитал об этом, я подумал: «Какой смысл?» Однако после того, как я изучил аргумент моего коллеги и внедрил систему шаблонов, которая использовала простой код PHP, но все же достиг конечной цели отделения бизнес-логики от логики представления (и примерно в 25 строках кода, не включая комментарии), я понял преимущества.

Эта система предоставляет разработчикам, таким как мы, доступ к множеству основных функций PHP, которые мы можем использовать для форматирования вывода — такие задачи, как форматирование даты, должны обрабатываться в шаблоне. Кроме того, поскольку все шаблоны представляют собой простые файлы PHP, такие программы кэширования байт-кода, как Zend Performance Suite и PHP Accelerator , могут автоматически кэшировать шаблоны (таким образом, их не нужно интерпретировать каждый раз, когда к ним обращаются). , Это только преимущество, если вы не забыли назвать файлы шаблонов так, чтобы эти программы могли распознавать их как файлы PHP (обычно вам просто нужно убедиться, что они имеют расширение .php).

Хотя я думаю, что этот метод намного превосходит типичные движки шаблонов, есть, конечно, некоторые проблемы. Самый очевидный аргумент против такой системы заключается в том, что код PHP слишком сложен, и что разработчикам не нужно изучать PHP. На самом деле, PHP-код такой же простой, как (если не проще, чем), синтаксис более продвинутых шаблонизаторов, таких как Smarty. Кроме того, дизайнеры могут использовать сокращенное обозначение PHP, например, <?=$var;?> . Это сложнее, чем {$var} ? Конечно, это на несколько символов длиннее, но если вы сможете к этому привыкнуть, вы получите всю мощь PHP без дополнительных затрат на анализ файла шаблона.

Во-вторых, и, возможно, что еще более важно, в шаблонах на основе PHP нет внутренней безопасности. Smarty предлагает возможность полностью отключить код PHP в файлах шаблонов. Это позволяет разработчикам ограничивать функции и переменные, к которым имеет доступ шаблон. Это не проблема, если у вас нет вредоносных дизайнеров. Однако, если вы разрешите внешним пользователям загружать или изменять шаблоны, представленное здесь решение на основе PHP не обеспечивает абсолютно никакой безопасности! Любой код может быть вставлен в шаблон и запущен. Да, даже print_r($GLOBALS) (который даст злоумышленнику доступ ко всем переменным в скрипте)!

Тем не менее, большинство проектов, с которыми я работал лично и профессионально, не позволяли конечным пользователям изменять или загружать шаблоны. Таким образом, это не было проблемой. Итак, теперь давайте перейдем к коду.

Примеры

Вот простой пример страницы со списком пользователей.

 <?php  require_once('template.php');   /**  * This variable holds the file system path to all our template files.  */  $path = './templates/';   /**  * Create a template object for the outer template and set its variables.  */  $tpl = & new Template($path);  $tpl->set('title', 'User List');   /**  * Create a template object for the inner template and set its variables.  The  * fetch_user_list() function simply returns an array of users.  */  $body = & new Template($path);  $body->set('user_list', fetch_user_list());   /**  * Set the fetched template of the inner template to the 'body' variable in  * the outer template.  */  $tpl->set('body', $body->fetch('user_list.tpl.php'));   /**  * Echo the results.  */  echo $tpl->fetch('index.tpl.php');  ?> 

Здесь следует отметить две важные концепции. Первый — это идея внутренних и внешних шаблонов. Внешний шаблон содержит HTML-код, который определяет основной вид сайта. Внутренний шаблон содержит HTML-код, который определяет область содержимого сайта. Конечно, вы можете иметь любое количество шаблонов в любом количестве слоев. Поскольку я обычно использую разные объекты шаблона для каждой области, проблем с пространством имен не возникает. Например, я могу иметь переменную шаблона с названием ‘title’ как во внутреннем, так и во внешнем шаблонах, не опасаясь конфликта.

Вот простой пример шаблона, который можно использовать для отображения списка пользователей. Обратите внимание, что специальный foreach и endforeach; синтаксис документирован в руководстве по PHP . Это совершенно необязательно.

Также вам может быть интересно, почему я заканчиваю имена файлов шаблонов расширением .php. Ну, многие решения для кэширования байт-кода PHP (например, phpAccelerator) требуют, чтобы файлы имели расширение .php, если они должны рассматриваться как файл PHP. Поскольку эти шаблоны являются файлами PHP, почему бы не воспользоваться этим?

 <table>     <tr>         <th>Id</th>         <th>Name</th>         <th>Email</th>         <th>Banned</th>     </tr>  <? foreach($user_list as $user): ?>     <tr>         <td align="center"><?=$user['id'];?></td>         <td><?=$user['name'];?></td>         <td><a href="mailto:<?=$user['email'];?>"><?=$user['email'];?></a></td>         <td align="center"><?=($user['banned'] ? 'X' : '&nbsp;');?></td>     </tr>  <? endforeach; ?>  </table>
 
 Вот простой пример layout.tpl.php (файл шаблона, который определяет, как будет выглядеть вся страница).

 <html>     <head>         <title><?=$title;?></title>     </head>      <body>          <h2><?=$title;?></h2>   <?=$body;?>      </body>  </html>
 
 И вот проанализированный вывод.

 <html>   <head>     <title>User List</title>   </head>    <body>      <h2>User List</h2>   <table>   <tr>     <th>Id</th>     <th>Name</th>     <th>Email</th>     <th>Banned</th>   </tr>   <tr>     <td align="center">1</td>     <td>bob</td>     <td><a href="mailto:bob@mozilla.org">bob@mozilla.org</a></td>     <td align="center">&nbsp;</td>   </tr>   <tr>     <td align="center">2</td>     <td>judy</td>     <td><a href="mailto:judy@php.net">judy@php.net</a></td>     <td align="center">&nbsp;</td>   </tr>   <tr>     <td align="center">3</td>     <td>joe</td>     <td><a href="mailto:joe@opera.com">joe@opera.com</a></td>     <td align="center">&nbsp;</td>   </tr>   <tr>     <td align="center">4</td>     <td>billy</td>     <td><a href="mailto:billy@wakeside.com">billy@wakeside.com</a></td>     <td align="center">X</td>   </tr>   <tr>     <td align="center">5</td>     <td>eileen</td>     <td><a href="mailto:eileen@slashdot.org">eileen@slashdot.org</a></td>     <td align="center">&nbsp;</td>   </tr>  </table>   </body>  </html> 

Кэширование

При таком простом решении реализация кэширования шаблонов становится довольно простой задачей. Для кэширования у нас есть второй класс, который расширяет исходный шаблонный класс. Класс CachedTemplate использует практически тот же API, что и исходный класс шаблона. Разница в том, что мы должны передать настройки кеша конструктору и вызвать fetch_cache() вместо fetch() .

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

Эта практика создает проблему уникальной идентификации файла кэша. Если сайт управляется центральным скриптом, который отображает выходные данные, основанные на переменных GET, то иметь только один кеш для каждого PHP-файла будет неудачно. Например, если у вас есть index.php?page=about_us , выходные данные должны полностью отличаться от той, которая будет возвращена, если пользователь с именем index.php?page=contact_us .

Эта проблема решается путем создания уникального cache_id для каждой страницы. Для этого мы берем фактический запрошенный файл и хешируем его вместе с REQUEST_URI (в основном, весь URL: index.php?foo=bar&bar=foo ). Конечно, хеширование CachedTemplate классом CachedTemplate , но важно помнить, что вы обязательно должны передавать уникальный cache_id при создании объекта CachedTemplate. Примеры следуют, конечно.

Использование настройки кэширования включает в себя следующие шаги.

  1. include() исходный файл шаблона
  • создать новый объект CachedTemplate (и передать путь к шаблонам, уникальный cache_id и время ожидания кэша)
  • проверить, кешируется ли уже контент
  • если это так, отображение файла и выполнение скрипта заканчивается
  • в противном случае выполните всю обработку и fetch() шаблон
  • fetch_cache() автоматически сгенерирует новый файл кеша
  • Этот сценарий предполагает, что ваши файлы кэша будут ./cache/ в ./cache/ , поэтому вы должны создать этот каталог и выполнить chmod чтобы веб-сервер мог писать в него. Также обратите внимание, что если вы обнаружите ошибку во время разработки какого-либо скрипта, эта ошибка может быть кэширована! Поэтому неплохо вообще отключить кэширование во время разработки. Лучший способ сделать это — передать ноль (0) в качестве времени жизни кеша — таким образом, кеш всегда истекает немедленно.

    Вот пример кеширования в действии.

     <?php  /**  * Example of cached template usage.  Doesn't provide any speed increase since  * we're not getting information from multiple files or a database, but it  * introduces how the is_cached() method works.  */   /**  * First, include the template class.  */  require_once('template.php');   /**  * Here is the path to the templates.  */  $path = './templates/';   /**  * Define the template file we will be using for this page.  */  $file = 'list.tpl.php';   /**  * Pass a unique string for the template we want to cache.  The template  * file name + the server REQUEST_URI is a good choice because:  *    1. If you pass just the file name, re-used templates will all  *       get the same cache.  This is not the desired behavior.  *    2. If you just pass the REQUEST_URI, and if you are using multiple  *       templates per page, the templates, even though they are completely  *       different, will share a cache file (the cache file names are based  *       on the passed-in cache_id.  */  $cache_id = $file . $_SERVER['REQUEST_URI'];  $tpl = & new CachedTemplate($path, $cache_id, 900);   /**  * Test to see if the template has been cached.  If it has, we don't  * need to do any processing.  Thus, if you put a lot of db calls in  * here (or file reads, or anything processor/disk/db intensive), you  * will significantly cut the amount of time it takes for a page to  * process.  *  * This should be read aloud as "If NOT Is_Cached"  */  if(!($tpl->is_cached())) {     $tpl->set('title', 'My Title');     $tpl->set('intro', 'The intro paragraph.');     $tpl->set('list', array('cat', 'dog', 'mouse'));  }   /**  * Fetch the cached template.  It doesn't matter if is_cached() succeeds  * or fails - fetch_cache() will fetch a cache if it exists, but if not,  * it will parse and return the template as usual (and make a cache for  * next time).  */  echo $tpl->fetch_cache($file);  ?> 

    Установка нескольких переменных

    Как мы можем установить несколько переменных одновременно? Вот пример, который использует метод, предоставленный Рикардо Гарсия.

     <?php    require_once('template.php');       $tpl = & new Template('./templates/');    $tpl->set('title', 'User Profile');       $profile = array(       'name' => 'Frank',       'email' => 'frank@bob.com',       'password' => 'ultra_secret'    );       $tpl->set_vars($profile);       echo $tpl->fetch('profile.tpl.php');    ?> 

    Связанный шаблон выглядит так:

     <table cellpadding="3" border="0" cellspacing="1">       <tr>           <td>Name</td>           <td><?=$name;?></td>       </tr>       <tr>           <td>Email</td>           <td><?=$email;?></td>       </tr>       <tr>           <td>Password</td>           <td><?=$password;?></td>       </tr>    </table> 

    И проанализированный вывод выглядит следующим образом:

     <table cellpadding="3" border="0" cellspacing="1">     <tr>       <td>Name</td>       <td>Frank</td>     </tr>     <tr>       <td>Email</td>       <td>frank@bob.com</td>     </tr>     <tr>       <td>Password</td>       <td>ultra_secret</td>     </tr>    </table> 

    Особая благодарность Рикардо Гарсии и Гарри Фьюксу за их вклад в эту статью.

    Ссылки по теме

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

    Исходный код класса шаблона

    И, наконец, класс Template.

     <?php    /**    * Copyright (c) 2003 Brian E. Lozier (brian@massassi.net)    *    * set_vars() method contributed by Ricardo Garcia (Thanks!)    *    * Permission is hereby granted, free of charge, to any person obtaining a copy    * of this software and associated documentation files (the "Software"), to    * deal in the Software without restriction, including without limitation the    * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or    * sell copies of the Software, and to permit persons to whom the Software is    * furnished to do so, subject to the following conditions:    *    * The above copyright notice and this permission notice shall be included in    * all copies or substantial portions of the Software.    *    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR    * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,    * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE    * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER    * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING    * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS    * IN THE SOFTWARE.    */       class Template {       var $vars; /// Holds all the template variables       var $path; /// Path to the templates          /**        * Constructor        *        * @param string $path the path to the templates        *        * @return void        */       function Template($path = null) {           $this->path = $path;           $this->vars = array();       }          /**        * Set the path to the template files.        *        * @param string $path path to template files        *        * @return void        */       function set_path($path) {           $this->path = $path;       }          /**        * Set a template variable.        *        * @param string $name name of the variable to set        * @param mixed $value the value of the variable        *        * @return void        */       function set($name, $value) {           $this->vars[$name] = $value;       }          /**        * Set a bunch of variables at once using an associative array.        *        * @param array $vars array of vars to set        * @param bool $clear whether to completely overwrite the existing vars        *        * @return void        */       function set_vars($vars, $clear = false) {           if($clear) {               $this->vars = $vars;           }           else {               if(is_array($vars)) $this->vars = array_merge($this->vars, $vars);           }       }          /**        * Open, parse, and return the template file.        *        * @param string string the template file name        *        * @return string        */       function fetch($file) {           extract($this->vars);          // Extract the vars to local namespace           ob_start();                    // Start output buffering           include($this->path . $file);  // Include the file           $contents = ob_get_contents(); // Get the contents of the buffer           ob_end_clean();                // End buffering and discard           return $contents;              // Return the contents       }    }       /**    * An extension to Template that provides automatic caching of    * template contents.    */    class CachedTemplate extends Template {       var $cache_id;       var $expire;       var $cached;          /**        * Constructor.        *        * @param string $path path to template files        * @param string $cache_id unique cache identifier        * @param int $expire number of seconds the cache will live        *        * @return void        */       function CachedTemplate($path, $cache_id = null, $expire = 900) {           $this->Template($path);           $this->cache_id = $cache_id ? 'cache/' . md5($cache_id) : $cache_id;           $this->expire   = $expire;       }          /**        * Test to see whether the currently loaded cache_id has a valid        * corrosponding cache file.        *        * @return bool        */       function is_cached() {           if($this->cached) return true;              // Passed a cache_id?           if(!$this->cache_id) return false;              // Cache file exists?           if(!file_exists($this->cache_id)) return false;              // Can get the time of the file?           if(!($mtime = filemtime($this->cache_id))) return false;              // Cache expired?           if(($mtime + $this->expire) < time()) {               @unlink($this->cache_id);               return false;           }           else {               /**                * Cache the results of this is_cached() call.  Why?  So                * we don't have to double the overhead for each template.                * If we didn't cache, it would be hitting the file system                * twice as much (file_exists() & filemtime() [twice each]).                */               $this->cached = true;               return true;           }       }          /**        * This function returns a cached copy of a template (if it exists),        * otherwise, it parses it as normal and caches the content.        *        * @param $file string the template file        *        * @return string        */       function fetch_cache($file) {           if($this->is_cached()) {               $fp = @fopen($this->cache_id, 'r');               $contents = fread($fp, filesize($this->cache_id));               fclose($fp);               return $contents;           }           else {               $contents = $this->fetch($file);                  // Write the cache               if($fp = @fopen($this->cache_id, 'w')) {                   fwrite($fp, $contents);                   fclose($fp);               }               else {                   die('Unable to write cache.');               }                  return $contents;           }       }    }    ?> 

    Еще одна важная вещь, которую стоит отметить в представленном здесь решении, это то, что мы передаем имя файла шаблона в вызов метода fetch() . Это может быть полезно, если вы хотите повторно использовать объекты шаблона без необходимости re-set() всех переменных.

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