Blitz Templates

$Id: blitz_ru.html,v 1.1 2005/09/18 19:42:10 fisher Exp $, raa sorry-no-spam phpclub d0t net

Содержание
Зачем ещё один парсер шаблонов?
Некоторые результаты тестов производительности
Инсталляция
Вводный курс
Параметры настройки
Синтаксис Blitz и API

Зачем ещё один парсер шаблонов?

Blitz родился весьма неоригинально, for fun. Однако, поигравшись с ним немного, мне показалось, что скорость, с которой он работает, и удобства, которые он предоставляет разработчику - стоят того, чтобы дать его поиграться коллегам. Основных "фишек" у Blitz три:
- написан как PHP-модуль на Си, и является одним из самых быстрых движков
- имеет простой и интуитивно понятный синтаксис
- позволяет структурировать код удобным и легко читаемым образом

Blitz поддерживает разделение и скрытие функционально различных частей шаблонов с помощью простого механизма: текст шаблона может содержать вызов пользовательского метода объекта, который этим шаблоном управляет. Таким образом достигается основная цель: шаблон не содержит большого количества блоков и контекстов, часто мешающих разобраться, что к чему. Напротив, даже в проекте со сложной логикой представления при правильном подходе шаблоны будут давать разработчику своеобразную "карту" всего проекта. Blitz также позволяет включать одни шаблоны в другие (аналог include) и поддерживает условный вывод переменных (аналог if).

Мне бы не хотелось здесь следовать академической традиции и проводить подробный анализ других проектов. В любом случае, если вы научились эффективно использовать сам PHP в качестве шаблонного движка, обходясь без сторонних продуктов и библиотек - вы счастливый человек. Вы используете самый эффективный с точки зрения производительности подход, и если он вам удобен - придерживайтесь его. Если нет - попробуйте Blitz. Возможно, он вас приятно удивит ;)

Некоторые результаты тестов производительности

С сожалению, мне неизвестна ни одна простая, универсальная и по-настоящему корректная методика анализа производительности шаблонных движков. А результатами любых искуственных, или как их ещё принято называть, синтетических, тестов пользоваться нужно максимально осторожно. Тем не менее, здесь приводятся результаты двух тестов. Первый тест - классический, измеряющий скорость выполнения циклических итераций одного и того же шаблона. Тест крайне простой, но позволяющий достаточно условно разделить группы движков на "нормальные", "медленные" и "никуда не годные". Число итераций и переменных в блоке было взято по умолчанию (9 переменных, 50 итераций), результаты этого теста приведены в таблице 1. Как легко видеть, Blitz по крайней мере аутсайдером не является.

тбл. 1
Тестовая машина(A): сервер XEON*2 2,4GHz (HT on) 2GB; linux php-4.3.10(fgci) zps nginx
blitz, php_templates: so-модули, CFLAGS: -g3 -O2

------------------------------------------------------
N         Engine name         Time         Percentage 
------------------------------------------------------
1           php              0.000544        100%     
2           blitz            0.001008        185%     
3           php_templates    0.001812        333%     
4           smarty           0.002006        369%     
5           str_replace      0.003713        683%     
6           phemplate        0.004514        830%     
7           fasttemplate     0.006835        1256%    
8           vtemplate        0.009565        1758%    
9           ultratemplate    0.012993        2388%    
10          templatepower    0.017056        3135%    
11          bugitemplate     0.019989        3674%    
12          phplib           0.028053        5157%    
13          profTemplate     0.043104        7924%    
14          xtemplate        0.048799        8970%    

Тестовая машина(B): PC PIV 2,8GHz (HT off) 1GB; linux-2.6.8 php-4.3.10 (Apache/1.3.33 static) zps
blitz, php_templates: so-модули, CFLAGS -g -O2

----------------------------------------------------------
N         Engine name          Time           Percentage  
----------------------------------------------------------
1            php              0.00045          100%
2            blitz            0.000834         185%
3            php_templates    0.001595         354%
4            smarty           0.001694         376%
5            str_replace      0.00373          829%
6            phemplate        0.004215         937%
7            fasttemplate     0.006139         1364%
8            vtemplate        0.008755         1946%
9            ultratemplate    0.012747         2833%
10           templatepower    0.018678         4151%
11           bugitemplate     0.019286         4286%
12           phplib           0.025478         5662%
13           profTemplate     0.045148         10033%
14           xtemplate        0.048137         10697%

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

Второй тест более приближен к полевым условиям. Он представляет собой тестирование некоторой динамической страницы, подготовленной с использованием разных движков, при помощи стардартной утилиты ab. Итак, у нас есть страница какого-то псевдо-портала, содержащая:
- ротирующиеся рекламные уши (3 шт)
- "полосатую" навигацию (~10 разделов)
- горячие новости (~10 шт)
- список пользователи онлайн (~20 шт)
- голосовалка с вариантами ответов (3 ответа)
- прочие переменные на странице (~5 шт)

Для тестирования было выбрано 4 подхода:

  • php mess: используется только php, причем весь код полностью упакован в один файл, представляя собой эдакую "кашу". Такой прием практически никогда не встречается в реальных больших проектах, но включен в тесты исключительно ради интереса, посколько очевидно является самым быстрым.
  • php includes: используется только php, функционально разные блоки (элементы списков) вынесены в отдельные файлы
  • php_templates: один шаблон, на каждый функциональный блок - контекст
  • blitz: 5 разных шаблонов на каждый функциональный блок

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

    тбл. 2
    Тестовая машина(B), см тбл. 1

    ab -n20000 -c256
    ----------------------------------------------------------
                  ZPS on     ZPS off    
    ----------------------------------------------------------
    php mess:      970        620
    php includes:  675        280        
    blitz:         620        410
    php_templates: 570        430
    
    К сожалению, я не имел возможности провести тесты для других шаблонных движков (впрочем, код для этого теста доступен, вы можете добавить в него решения исходной задачи с иcпользованием любых других средств). Поэтому ограничусь обобщенной интерпретацией этих результатов. То, что native PHP-код вместе с акселератором всегда будут быстрее прочих решений - очевидно. Правда, следует особенно подчеркнуть, что native в этом смысле - именно написанный программистом самим, а не "скомпилированный". В этом легко убедиться, заглянув внутрь любого "скомпилированного" шаблона: как правило, их код состоит из многомерных, довольно сложных для выполнения конструкций, значительно сложнее, чем написанный правильными руками код ;) Поскольку разница между blitz и "правильным" методом php includes не является кардинальной, а все синтетические тесты позволяют лишь выявить группы приблизительно равных, можно с определенной долей уверенности считать методы разработки с использованием php, blitz и php_templates примерно одинаковыми по производительности.

    Следует также принять во внимание, что в реальном проекте разница в скорости между различными методами скорее всего будет ещё меньше. Во-первых это связано с тем, что значительное время будет тратиться на работу с источниками данных (СУБД, различные сервисы и проч.). Во-вторых, отношение "количества" кода, относящегося к уровню представления, и прочего кода будет совершенно иным. Грубо говоря, view_code = full_code для тестов и пусть выигрыш на синтетическом тесте составляет даже десятки процентов. Но в реальном проекте часто выполняется соотношение view_code << full_code, и поэтому выигрыш на уровне представления уже почти ничего не даст. Как вы могли заметить, почти все тесты были проведены с использованием акселератора из ZPS. Вряд ли сейчас можно представить крупный проект, в котором не используется акселератор, однако, акселератор акселератору рознь. И вполне возможно вы получите совершенно иные результаты при использовании, например, eAccelerator'a. В-общем, призываю вас не полагаться полностью на приведенные результаты. Скачивайте тесты, экспериментируйте на реальных задачах, и выбирайте те решения, которые дают выигрыш в вашем проекте.

    Инсталляция

    Blitz - расширение PHP, поставляемое пока исключительно в исходных кодах, поэтому его инсталляция состоит из обычных шагов по сборке расширешия:
    bash> tar zxvf blitz.tar.gz
    bash> cd blitz
    bash> phpize
    bash> make
    bash> make install
    
    После этого вы, возможно, захотите отредактировать свой php.ini, включив blitz в список расширений:
    extension=blitz.so

    Сборка blitz тестировалась на нескольких *nux-платформах, и пока случаев, чтобы были какие-то проблемы при сборке, неизвестно ;) Под windows blitz пока не собирался.

    Вводный курс

    Как и во многих других шаблонных движках двумя базовыми сущностями проекта, которые использует blitz, являются собственно шаблон и объект, управляющий выполнением этого шаблона. В шаблоне допускаются три вида конструкций
  • переменная, {{ $my_var }}
  • вызов спец-метода, {{ include('some_other_template.tpl') }} или {{ if($my_var,'00ffff','00ffee') }}
  • вызов пользовательского метода, {{ my_method }}

    Переменные. Следующий код демонстрирует работу с переменными:

    Ex. 1, file ex1A.tpl:
    Это некоторый тест для двух переменных: {{ $a }} и {{ $b }}, номер итерации: {{ $i }}
    
    Ex. 1, file ex1B.php
    <?
    
    $T = new Blitz('ex1A.tpl');
    $i = 0;
    $i_max = 10;
    for ($i = 0; $i<$i_max; $i++) {
        echo $T->parse(
            array(
                'a' => 'var_'.(2*$i),
                'b' => 'var_'.(2*$i+1),
                'i' => $i
            )
        );
    }
    
    ?>
    Ex.1 Output:
    Это некоторый тест для двух переменных: var_0 и var_1, номер итерации: 0
    Это некоторый тест для двух переменных: var_2 и var_3, номер итерации: 1
    Это некоторый тест для двух переменных: var_4 и var_5, номер итерации: 2
    Это некоторый тест для двух переменных: var_6 и var_7, номер итерации: 3
    Это некоторый тест для двух переменных: var_8 и var_9, номер итерации: 4
    Это некоторый тест для двух переменных: var_10 и var_11, номер итерации: 5
    Это некоторый тест для двух переменных: var_12 и var_13, номер итерации: 6
    Это некоторый тест для двух переменных: var_14 и var_15, номер итерации: 7
    Это некоторый тест для двух переменных: var_16 и var_17, номер итерации: 8
    Это некоторый тест для двух переменных: var_18 и var_19, номер итерации: 9
    
    
    Класс Blitz - внутренний класс расширения, управляющий шаблоном. Первый и единственный аргумент конструктора класса - имя шаблона. Вместо вызова parse($arr_params) вы можете использовать set($arr_params) и parse() без аргументов.

    Спец-методы. Следующий код демонстрирует использование include:

    Ex. 2, file ex2A.tpl:
    Мама {{ include('ex2B.tpl') }} раму
    
    Ex. 2, file ex2B.tpl:
    мыла
    
    Ex. 2, file ex2С.php:
    <?
    
    $T = new Blitz('ex2A.tpl');
    echo $T->parse();
    echo "\n";
    
    ?>
    Ex.2 Output:
    Мама мыла раму
    
    
    Если вы включаете один шаблон в другой, то включаемый шаблон наследует все переменные внешнего шаблона:
    Ex. 3, file ex3A.tpl:
    переменная a = {{ $a }}
    внутренний шаблон: {{ include('ex3B.tpl') }}
    переменная b = {{ $b }}
    
    Ex. 3, file ex3B.tpl:
    /* переменная a = {{ $a }}, переменная b = {{ $b }} */
    
    Ex. 3, file ex3С.php:
    <?
    
    $T = new Blitz('ex3A.tpl');
    $T->set(array('a' => 'a_value', 'b' => 'b_value'));
    echo $T->parse();
    echo "\n";
    
    ?>
    Ex.3 Output:
    переменная a = a_value
    внутренний шаблон: /* переменная a = a_value, переменная b = b_value */
    переменная b = b_value
    
    В шаблоне также можно использовать псевдо-оператор if. На самом деле, это такой же спец-метод, отображающий в зависимости от истинности предиката либо один аргумент, либо другой. Cуществует укороченная форма if, без третьего аргумента - аналогичная полной форме с пустым третьим аргументом: if($a,$b) = if($a,$b,'');
    Ex. 4, file ex4A.tpl:
    {{ $num }}. {{ $name }} {{ if($rip,'[R.I.P.]') }}
    
    Ex. 4, file ex4B.php:
    <?
    
    T = new Blitz('ex4A.tpl');
    $character = array(
        array(1,'The Dude',0),
        array(2,'Walter Sobchak',0),
        array(3,'Donny',1),
        array(4,'Maude Lebowski',0),
        array(5,'The Big Lebowski',0),
        array(6,'Brandt',0),
        array(7,'Jesus Quintana',0),
    );
    
    foreach ($character as $i => $data) {
       echo $T->parse(
           array(
               'num' => $data[0],
               'name' => $data[1],
               'rip' => $data[2]
           )
       );
    }
    
    ?>
    Ex.4 Output:
    1. The Dude
    2. Walter Sobchak
    3. Donny [R.I.P.]
    4. Maude Lebowski
    5. The Big Lebowski
    6. Brandt
    7. Jesus Quintana
    
    В данном примере использована укороченная форма if.

    Пользовательские методы. Возможность включать в шаблон пользовательские методы - самая интересная с точки зрения организации хорошего и удобно читаемого кода. До сих пор в примерах использовался стандартный класс Blitz, никакими новыми методами не обладающий. Однако, если создать объект класса-наследника Blitz, который предоставляет некоторый метод my_test, в шаблоне можно использовать вызов этого метода ровно с таким же названием:

    Ex 5, file ex5a.tpl:
    пример вызова пользовательского метода: {{ my_test }}
    
    Ex 5, file ex5B.php:
    <?
    
    class BlitzTemplate extends Blitz {
    
        function BlitzTemplate($t) {
            parent::Blitz($t);
        }
    
        function my_test() {
            return 'user method called ('.__CLASS__.','.__LINE__.')';
        }
    }
    
    $T = new BlitzTemplate('ex5A.tpl');
    echo $T->parse();
    
    ?>
    
    Ex 5, Output:
    пример вызова пользовательского метода: user method called (blitztemplate,10)
    

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

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

    class BlitzTemplate extends Blitz {
        var $data;
        var $TItem;
        function BlitzTemplate($t,$titem) {
            parent::Blitz($t);
            $TItem = new Blitz($titem);
        }
    
        function set_data() {
            // some code
        }
    
        function my_test() {
            $result = '';
            foreach ($this->data as $i_data) {
                $result .= $TItem->parse($i_data);
            }
            return $result;
        }
    }
    
    $T = new BlitzTemplate('main.tpl','item.tpl');
    // $bla_bla = ...
    $T->set_data($blabla);
    echo $T->parse();
    
    

    Этот метод будет работать, но не очень хорош по двум причинам. Во-первых, $TItem является совершенно отдельным объектом, никак не связанным с $T. Blitzу несколько сложнее переключаться с одного объекта на другой, нежели выполнять все операции через один и тот же объект. Во-вторых, $TItem не будет наследовать установленные переменные из $T, их при необходимости нужно будет протягивать самостоятельно, а также внутри $TItem нельзя использовать методы $T. Поэтому более правильным будет использование встроенного метода include:

    Ex.6, file ex6A.tpl:
    parent value: {{ $parent_val }}
    child_value: {{ $child_val }}
    ===========================================================
    {{ test_include }}
    ===========================================================
    parent value: {{ $parent_val }}
    child_value: {{ $child_val }}
    
    Ex.6, file ex6B.tpl:
    parent method: {{ my_test }}
    child value: {{ $child_val }}
    parent value: {{ $parent_val }}
    
    Ex.6, file ex6C.php:
    <?
    
    class BlitzTemplate extends Blitz {
        var $titem;
    
        function BlitzTemplate($t,$titem) {
            parent::Blitz($t);
            $this->set(array('parent_val' => 'some_parent_val'));
            $this->titem = $titem;
        }
    
        function my_test() {
            return 'user method called ('.__CLASS__.','.__LINE__.')';
        }
    
        function test_include() {
            $result = '';
            while($i++<3) {
                $result .= $this->include($this->titem,array(
                    'child_val' => 'i_'.$i
                ));
            }
            return $result;
        }
    
    }
    
    $T = new BlitzTemplate('ex6A.tpl','ex6B.tpl');
    echo $T->parse();
    
    ?>
    
    Ex.6, Output:
    parent value: some_parent_val
    child_value:
    ===========================================================
    parent method: user method called (blitztemplate,13)
    child value: i_1
    parent value: some_parent_val
    parent method: user method called (blitztemplate,13)
    child value: i_2
    parent value: some_parent_val
    parent method: user method called (blitztemplate,13)
    child value: i_3
    parent value: some_parent_val
    
    ===========================================================
    parent value: some_parent_val
    child_value: i_3
    
    При первой обработке шаблона структура всех его тэгов сохраняется, поэтому при последующих вызовах шаблон снова не анализируется. Обратите внимание на то, что до выполнения метода test_include переменная child_value пуста и не "видна" в шаблоне, но после выполнения видна и содержит последнее установленное значение. Это поведение аналогично тому, что поисходит при выполнении php-кода, если бы вместо test_include у нас был include некоторого php-файла, внутри которого бы инициализировалась новая переменная. Внутри внешнего кода до include она имела бы неопределенное значение, но после - уже нет. На самом деле при вызове include сначала все параметры вызова include добавляются к уже установленным параметрам шаблона, и уже после этого происходит выполнение кода, поэтому ничего удивительного в таком поведении нет. Эту особенность следует иметь ввиду, чтобы случайно не "затереть" ранее установленную переменную.

    Параметры настройки

    Вы можете изменять следующие параметры настройки (php.ini):
  • строка, открывающая тэг (blitz.tag_open), по умолчанию равный '{{'
  • строка, закрывающая тэг (blitz.tag_close), по умолчанию равный '}}'
  • префикс для переменных (blitz.var_prefix), по умолчанию равный '$'

    Синтаксис Blitz и API

    Синтаксис шаблонов

    Тэги шаблонов blitz имеют обобщенный вид [TAG_OPEN][INNER_CODE][TAG_CLOSE]. TAG_OPEN,TAG_CLOSE - строки, открывающие и закрывающие тэг, по умолчанию '{{' и '}}'. INNER_CODE может содержать лексемы:

  • переменная шаблона, [VAR_PREFIX][VAR_NAME]. VAR_PREFIX - префикс, указывающий на то, что это переменная, по умолчанию равен '$'. VAR_NAME - название переменной, разрешены символы [a-zA-Z0-9_-].
  • встроенный метод if(ARG1,ARG2,[ARG3]). В зависимости от "истинности" предиката ARG1 либо отображает ARG2, либо ARG3. ARG(1-3) могут быть: строкой в одинарных или двойных кавычках, числом, или переменной. В качестве escape-символа в строковых константах используется символ '\'. В зависимости от типа аргумента условие считается выполненным если: это целое или десятичное число и оно не равно нулю, это строка и она не пуста, это булева переменная TRUE. Возможна укороченная форма if(ARG1,ARG2) идентичная записи if(ARG1,ARG2,'').
  • встроенный метод include(TEMPLATE_NAME), выполнить другой шаблон TEMPLATE_NAME. TEMPLATE_NAME может быть только строковой константой (возможно, в последующих релизах будут поддерживаться и переменные шаблона, содержащие имена файлов). Включаемый шаблон наследует все переменные родительского шаблона и может содержать вызовы методов родительского шаблона.
  • вызов пользовательского метода [METHOD_NAME], разрешены символы [a-zA-Z0-9_-]. Выполняет метод METHOD_NAME класса контроллера шаблона, если этот метод определен в классе. Возвращаемое методом значение конвертируется в строку и подставляется в результат. Если метода не существует, результат выполнения будет пустой строкой.
  • Пустые символы (пробелы и симболы табуляции) от конца открывающего тэга до первого непустого символа и от последнего непустого символа до конца закрывающего тэга анализатором пропускаются, в результат не включаются. Общая длина лексемы INNER_CODE с учетом пустых символов не должна превышать BLITZ_MAX_LEXEM_LEN=1024 символа. Возможно, в последующих релизах эти ограничения будут изменены или вовсе сняты.

    Пример:

    переменная: {{ $a }}
    переменная: {{ $b }}
    вставка шаблона: {{ include("my.tpl") }}
    вызовы метода if: 
    {{ if(77,'test if') }}
    {{ if(88,"test if") }}
    {{ if('','test if') }}
    {{ if($a,'test\' if') }}
    {{ if($a,6123) }}
    {{ if(77," \"test if",'test if') }}
    {{ if('hello','test if','test if') }}
    {{ if($a,'test if3','test if') }}
    {{ if($a,1789,'test if3') }}
    {{ if($a,1789,$b) }}
    {{ if($b,$c,1789) }}
    {{ if($d,'test if',$a) }}
    вызов динамического метода: {{ some_method }}
    

    Класс Blitz

    Конструктор:
    Blitz(TEMPLATE_NAME), TEMPLATE_NAME - полный путь до файла шаблона

    Методы:

    set(ARRAY), установить значения переменных шаблона. ARRAY - хэш имя_переменной => значение. В случае ошибки возвращает FALSE, в остальных случаях - TRUE.

    parse([ARRAY]), выполнить шаблон. ARRAY - необязателтьный параметр, по смыслу тот же хэш имя_переменной => значение, что и в методе set (параметр используется для более компактного вызова, минуя set). Возвращает либо переменную, содержащую результат выполнения шаблона, либо FALSE.

    include(TEMPLATE_NAME,[ARRAY]), выполнить другой шаблон TEMPLATE_NAME с параметрами ARRAY. Перед выполнением осуществляется объединение уже установленных переменных шаблона с параметрами ARRAY, код вызываемого шаблона "наследует" все переменные родительского шаблона и может содержать вызовы методов родительского шаблона. Возвращает либо переменную, содержащую результат выполнения шаблона, либо FALSE.

    Пример (Ex.6):

    <?
    
    class BlitzTemplate extends Blitz {
        var $titem;
    
        function BlitzTemplate($t,$titem) {
            parent::Blitz($t);
            $this->set(array('parent_val' => 'some_parent_val'));
            $this->titem = $titem;
        }
    
        function my_test() {
            return 'user method called ('.__CLASS__.','.__LINE__.')';
        }
    
        function test_include() {
            $result = '';
            while($i++<3) {
                $result .= $this->include($this->titem,array(
                    'child_val' => 'i_'.$i
                ));
            }
            return $result;
        }
    
    }
    
    $T = new BlitzTemplate('ex6A.tpl','ex6B.tpl');
    echo $T->parse();
    
    ?>