Blitz templates, template engine extension for PHP

fast and powerfull template engine for very big internet projects

Alexey A. Rybak (c) 2005 - 2012


Sponsored link: Badoo is hiring

Positions for PHP/MySQL, C/C++, flont-end developers, QA engineers, DBA and others - in our Moscow office

Downloads

0.9.1 version: blitz-0.9.1.tar.gz
Windows versions:
0.8.5, 0.8.4, 0.8.3 thanx to Alex Sladkov for these builds.

Usefull links

Browse: new releases on github or old sources (up to 0.8.17), all win32 binaries
Docs/Translations (current doc is most latest!): quick geek, english (old!), russian (old!), chinese (old!)
Bug Tracker
User list: blitz-php at googlegroups d0t c0m
Developer's contact: alexey d0t rybak at gmail d0t c0m


Recent Blitz news

Please see the CHANGES file for more details.

Who uses Blitz?

  • Badoo (Alexa rank #117)
  • Habrahabr (Alexa rank #758)
  • Mamba (Alexa rank #1525)
  • You too? Send me your project name, alexey d0t rybak at gmail d0t com. Thanx!


  • Thoinks, Moite!

    Australia became 2rd in our traffic rating (after Russia - which I just don't count as it's obvious - but leaving Ukraine far below!). Thanks a lot, folks! Also many thanks to users from Ukraine, Belarus, Netherlands, Kazakhstan, UK, USA, Brazil, Spain, France and Poland.

    Developers guide

    0. Quick geek tutorial

    If you are experienced developer who just needs to start working with Blitz very quickly - read this "Quick Geek Tutorial". It's quite old though - please refer to the Template or View API for new methods and features.

    1. Introduction

    The most probably question you ask, viewing this document for the first time is: "What? Yet another template engine? What for?". Here comes a very short explanation.

    Blitz is a templating engine with two main features:


    These are my own development values which were formed through my own experience with large-scale internet projects. Note, this all comes mostly from "management", not "programming", and has influence on large projects particularly. Let me explain this all briefly.

    Is "fast" important? Talking about the speed I'm sure 99,9% of projects are not CPU-bound. But large projects are. Most of top projects have thousands frontends serving billions requests per day (top of the top has even much more). Performance management is a field of special importance here. Usually storage/application services are tuned as much as possible and CPU usage on frontend boxes is, well, not bottleneck, but one of very important degrees of freedom. Crooked solutions which scale badly will significantly increase your total cost. From my personal experience presentation code may eat up to 30-50%% CPU for the dynamic page generation in a "good" project and up to 100% in "bad". Think of that: your frontend CPU spends most of the time just building result HTML! It is very important to decrease this value. With Blitz you won't spend much CPU on presentation level (see benchmark results below).

    Is "clear" important? Talking about the simplicity of presentation code I'm absolutely sure 99,9% of projects can be written in plain PHP and happily supported through years. Plain PHP is probably the best solution from the performance point of view as well. There's one problem though. Supporting the mess of PHP and HTML in a big project is usually just a nightmare. Large projects usually have large codebase and a lot of problems appear while trying to manage PHP+HTML spaghetti in presentation code. This happens because modern, personalized applications have very complex presentation logic and this all tend to change intensively. This logic should be written, owned and supported by developer (I also suppose here that HTML/CSS competence is much different from programming so developers better don't touch this stuff at all - and vice versa). But when you have PHP code together with HTML and Javascript in templates - the ownership and the responsibility is too fuzzy. That's why I always wanted HTML/JS guys to keep their hands off the presentation logic. I do know what happens otherwise when this part of application is "shared". That's why I wanted to maximally separate HTML and view logic. I wanted to see the code, with no HTML/Smarty/PHP/Whatever spaghetti mess. I wanted developers to work with the code, with no HTML or JS around. Well, that is a some kind of management philosophy. You just feel this "right" - or not :)))

    I started this project in 2005 as a replacement of php_templates by Maxim Poltarak, a very fast PHP template engine which was popular in ealy 200x mostly among developers in former USSR countries. This engine was unjustly rejected by PHP team long time ago (they didn't let it into PECL for absolutely childish reasons) so the project was frozen and got no further developments. I took most of "design" concepts from php_templates and thank Maxim for his great project.

    2. Installation

    Blitz is a PHP extension distributed in source code (Win32 users can download compiled DLL's).

    To extract the code and build the extension just follow these simple steps:
    tar zxvf blitz-0.6.10.tar.gz
    cd blitz-0.6.10
    phpize
    ./configure
    make install
    
    To test blitz run the test script run-tests.sh. You need to edit this file to change the path to the standart PHP test routine run-tests which comes with PHP. Running the tests you will get something like this:
    fisher@fisher:~/prj/blitz> ./run-tests.sh
    =====================================================================
    PHP         : /usr/local/bin/php5
    PHP_SAPI    : cli
    PHP_VERSION : 5.3.2-dev
    ZEND_VERSION: 2.3.0
    PHP_OS      : Linux - Linux fisher 2.6.11.4-20a-smp #1 SMP Wed Mar 23 21:52:37 UTC 2005 i686
    INI actual  : /local/php_5_3_fpm/lib/php-cli.ini
    More .INIs  :
    CWD         : /home/fisher/prj/blitz
    Extra dirs  :
    VALGRIND    : Not used
    =====================================================================
    Running selected tests.
    PASS block [tests/block.phpt]
    PASS Bug #83 (double free on include()) [tests/bug83.phpt]
    PASS clean [tests/clean.phpt]
    PASS comments [tests/comments.phpt]
    PASS contexts [tests/context.phpt]
    PASS date output wrapper [tests/date.phpt]
    PASS double Blitz object initialization [tests/double_init.phpt]
    PASS report duplicate contexts [tests/duplicates_001.phpt]
    PASS broken templates 001 [tests/error_001.phpt]
    PASS broken templates 002 [tests/error_002.phpt]
    PASS errors and warnings: syntax [tests/errors1.phpt]
    PASS errors and warnings: execution [tests/errors2.phpt]
    PASS escape output wrapper [tests/escape.phpt]
    PASS fetch#1 [tests/fetch1.phpt]
    PASS fetch#2 [tests/fetch2.phpt]
    PASS fetch#3 [tests/fetch3.phpt]
    PASS get context [tests/get_context.phpt]
    PASS has context [tests/has_context.phpt]
    PASS if/unless predefined methods [tests/if.phpt]
    PASS conditional contexts (multi-line conditions): if/end and unless/end [tests/if_context.phpt]
    PASS controller include method [tests/include-method.phpt]
    PASS predefined methods: include [tests/include.phpt]
    PASS include with context iteration [tests/include_ctx.phpt]
    PASS multiple include cache test [tests/include_multi.phpt]
    PASS ini-values settings test [tests/ini.phpt]
    PASS nonexistant path iteration [tests/iterate_nonexistant.phpt]
    PASS user-defined methods [tests/method.phpt]
    PASS method call from inner include [tests/mfi.phpt]
    PASS mix #1 [tests/mix1.phpt]
    PASS mix #2 [tests/mix2.phpt]
    PASS mix #3 [tests/mix3.phpt]
    PASS mix #4 [tests/mix4.phpt]
    PASS mix #5 [tests/mix5.phpt]
    PASS mix #6 [tests/mix6.phpt]
    PASS numerical and non-numerical keys in the iteration set [tests/non_num_iter.phpt]
    PASS parse with iteration set [tests/parse_with_iterations.phpt]
    PASS relative path (blitz.path) [tests/path.phpt]
    PASS partial php_templates syntax compability [tests/phpt_compability.phpt]
    PASS plugins [tests/plugins.phpt]
    PASS predefined loop variables: $_total, $_num, $_even, $_odd, $_first, $_last [tests/predefined.phpt]
    PASS returning non-strings from user methods [tests/return_non_string.phpt]
    PASS scope lookup test #1 [tests/scope.phpt]
    PASS scope lookup test #2 [tests/scope2.phpt]
    PASS set and get [tests/set_and_get.phpt]
    PASS mixed set [tests/set_mixed.phpt]
    PASS remove empty spaces around context tags [tests/spaces.phpt]
    PASS variables [tests/var.phpt]
    PASS unprefixed variables syntax [tests/var_no_prefix.phpt]
    PASS {{ $hash.sub.key }} syntax [tests/var_path.phpt]
    PASS iterate after wrong previous iterations [tests/wrong_iterations.phpt]
    =====================================================================
    Number of tests :   50                50
    Tests skipped   :    0 (  0.0%) --------
    Tests warned    :    0 (  0.0%) (  0.0%)
    Tests failed    :    0 (  0.0%) (  0.0%)
    Expected fail   :    0 (  0.0%) (  0.0%)
    Tests passed    :   50 (100.0%) (100.0%)
    ---------------------------------------------------------------------
    Time taken      :    1 seconds
    =====================================================================
    
    If Blitz was build as shared module you will probably need to edit your php.ini and include Blitz in extension list:
    extension=blitz.so 
    

    3. Configuration

    This list is based on 0.7.* versions. See the difference in old 0.6.* below.

  • blitz.var_prefix - variable prefix, default is "$"
  • blitz.tag_open - open tag, default is "{{" (double brackets are used not to confuse with single CSS brackets)
  • blitz.tag_close - close tag, default is "}}"
  • blitz.tag_open_alt - alternative open tag, default is "<!-- " (Note: 5 symbols with space in the end)
  • blitz.tag_close_alt - alternative close tag, default is " -->" (Note: 4 symbols with space in the beginning)
  • blitz.comment_open - open comments tag, default is "/*"
  • blitz.comment_close - close comment tag, default is "*/"
  • blitz.enable_alternative_tags - use 0/1 to disable/enable alternative tags, "1" by default
  • blitz.enable_comments - use 1/0 to enable/disable comments, "0" by default
  • blitz.path - prefix filenames when they don't start with '/' (UNIX/Linux) or '[A-Z]:\' (Windows), default is ""
  • blitz.disable_include - disable includes for true paranoid, "0" by default
  • blitz.remove_spaces_around_context_tags - remove annoying spaces and linebreaks around context tags, "1" by default
  • blitz.warn_context_duplicates - warn if there are contexts with the same name, "0" by default
  • blitz.check_recursion - check recursion, 0/1, "1" by default
  • blitz.charset - charset for "escape" method (used in internal php_escape_html_entities call), "" by default
  • blitz.scope_lookup_limit - the depth of "upper stack" lookups when variable is not found in current context scope, default is "0"

    Please note some differences with old 0.6.* versions:

  • blitz.tag_open_alt and blitz.tag_close_alt were called blitz.phpt_ctx_left and blitz.phpt_ctx_right correspondingly
  • blitz.remove_spaces_around_context_tags was switched off by default
  • blitz.comment_open, blitz.comment_close, blitz.enable_alternative_tags, blitz.enable_comments, blitz.charset, blitz.scope_lookup_limit were added in 0.7.* only.

    4. Basics: variables, contexts, methods

    4.1. Variables

    The "view" component of your application will be built by templates and views("views" are also referred as template controllers. Let's start with the following example:
    $View = new Blitz();
    $View->load('Where is the {{ $what }}, Lebowski?');
    $View->display(array('what' => 'money'));
    
    This code produces the string 'Where is the money, Lebowski?'. Here we have an anonymous template loaded as string and a template controller $View.

    Normally you'll load templates from files:
    $View = new Blitz('some.tpl');
    
    But load() method is very usefull to write tests quickly - no need to create separate template file.

    Template is an HTML file with some very simple syntax. No complex code mixed with HTML. Controller is a template logic master, an instance of Blitz class which operates template from opening to the final result. Template controller is not your application controller which stands for "C" in MVC. No HTML code inside template controller is needed.

    4.2. Contexts

    The reason why Blitz objects are called "template controllers" is simple. From the very early days template language in Blitz was designed to be as simple ("non-programming") as possible. For example, there is still no "for" or "foreach" statement in Blitz. This surely doesn't mean you can't do any looping :) But you have to loop from your PHP-code, this is called "passive" templates (in Blitz you can do a lot of "active" templating as well - conditions, callbacks, plugins - but loops like "foreach", complex expressions, all these "programming" statements are still under the law).

    To make a loop you need to use blocks in your templates. Block, or context, is a part of template marked by {{ BEGIN }} and {{ END }} tags. Blocks can be populated on demand but hidden by default:
    $View = new Blitz();
    $View->load('hello {{ BEGIN block }} {{ $name }} {{ END }}');
    $T->display(array('block' => array('name' => 'Dude')));
    
    This template produces just a "hello " string by default and the block is hidden. When you set 'block' key into template variables Blitz executes the block and the result is "hello Dude ". The value of 'block' is a hash. Blitz treats this hash as parameters. If you put an array of hashes - Blitz executes the block for every array element. Blocks are also referred as contexts. An execution of block is called "iteration".

    To make the code simpler to read one can put a block name after END statement: {{ END block }}.

    There is a special 'block' method that will perobably make your code a bit easier to read:
    $View = new Blitz();
    $View->load('hello {{ BEGIN block }} {{ $name }} {{ END block }}');
    $View->block('/block', array('name' => 'Dude'));
    $View->display();
    
    To make lists you just need to iterate blocks several times:
    foreach (array('Dude', 'Donny', 'Sobchak') as $i_name) {
        $T->block('/block', array('name' => $i_name);
    }
    
    Being placed instead of a single block call in the previous example this will output
    "Hello  Dude  Donny  Sobchak "
    
    Note the number of spaces between names. This is not a mistake, everything is absolutely correct: for "Dude" first space comes from 'hello_' and second space comes from '_{{ $name'; "Donny" has two spaces from previous iteration ('$name }}_' = 'Dude_') and an additional space from '_{{ $name'; and finally "Sobchak" has two spaces before for the same reasons as "Donny" and one space after becase of '$name }}_'.

    Every block is identified by it's path like "/block". A block placed inside another has path /parent/child. Even things like '../../some/path' work.

    As you could see there can be two ways of "controlling" template blocks. The first is to use "block" methods (there are also additional methods to set up active context, to iterate the current context and so on). The second is just to set up complex data structures with keys named equal to block names. In previous examples we set up a hash for 'block'. To produce a list you just need this hash to be an array of hashes:
    $View = new Blitz();
    $View->load('hello {{ BEGIN block }} {{ $name }} {{ END }}');
    $T->display(
        array('block' => array(
            array('name' => 'Dude'),
            array('name' => 'Donny'),
            array('name' => 'Sobchak'),
        ))
    );
    
    Let's take a break and think what we have here. Single block iteraton was just like "if". Multiple block iteration was like "foreach". This is all quite tricky for the first time, but if you have "math" mind - this all becomes clear and simple very soon. Note that you don't need to add any language constructs into the template. What you do - just play with template controller, iterating template blocks.

    Honestly, using blocks as "if" substitution is crazy :) See what you have to do if you just need a simple comma separated list in the previous example. At first you have to add a special separator block:
    hello {{ BEGIN block }}{{ BEGIN comma }},{{ END }} {{ $name }} {{ END }}
    
    then you have to add this code to iterate this when needed:
    $need_comma = FALSE;
    foreach (array('Dude', 'Sobchak', 'Donny') as $i_name) {
       if ($need_comma) {
           $T->block('/block/comma');
       } else 
           $need_comma = TRUE;
       }
       $T->block('/block', array('name' => $i_name);
    }
    
    When your logic is simple you can use a very basic "if" statement to make things much easier:
    hello {{ BEGIN block }}{{ if($_first,'',',') }} {{ $name }} {{ END }}.
    
    And the code remains as simple as:
    foreach (array('Dude', 'Donny', 'Sobchak') as $i_name) {
        $T->block('/block', array('name' => $i_name);
    }
    
    You can use any variable name instead of $_first but then you need to set this variable manually from controller. $_first is a predefined variable, equal to 1 when it's the first block iteration and 0 otherwise. Other predefined loop variables are: $_last, $_total, $_num, $_even, $_odd. Most of these variables have clear meaning, $_total is a number of context iterations and $_num is a current number starting with 1, $_even and $_odd are 1 or 0 depending on if the current iteration value is even or odd.

    Here we used a short form of "if" as a "method". You can use condition blocks as well:
    hello {{ BEGIN block }} {{ UNLESS $_first }}, {{ END }} {{ $name }} {{ END }}.
    
    The full version of IF/UNLESS block is a IF(UNLESS)/ELSEIF/.../ELSEIF/ELSE/END set. UNLESS means "if not" condition. Expressions are not currently supported, but simple expression support is included into the development plan.

    4.3. Variable scope and contexts

    Let's finally take a complex list example
    $body = <<<BODY
    {{ BEGIN list }}
    ==================================================
    list #{{ \$_num }}, x = {{ \$x }}
    {{ UNLESS sublist }}
       empty
    {{ ELSE }}
    --------------------------------------------------
    {{ BEGIN sublist }}
      row #{{ \$_num; }} v = {{ \$v }}, x = {{ \$x }}
    {{ END }}
    {{ END }}
    {{ END }}
    BODY;
    
    $T = new Blitz();
    $T->load($body);
    
    $data = array(
        'list' => array(
            0 => array(
                'x' => 'first'
            ),
            1 => array(
                'x' => 'second',
                'sublist' => array(
                    0 => array('v' => 'a'),
                    1 => array('v' => 'b'),
                )
            )
        )
    );
    
    $T->display($data);
    
    This code will produce:
    ==================================================
    list #1, x = first
       empty
    ==================================================
    list #2, x = second
    --------------------------------------------------
      row #1 v = a, x =
      row #2 v = b, x =
    
    See that 'x' value is empty in a 'sublist' context. For the performance reasons Blitz checks only current iteration parameters and this hash doesn't have any 'x' key. To have variable inside block you have three options:
  • pass it via params manually for every context iteration
  • pass it as global once
  • force Blitz to make "upper" lookups when parameter was not found

    Globals are set by setGlobals() method:
    // body remains the same
    ...
    
    $T = new Blitz();
    $T->load($body);
    
    // data remains the same
    ... 
    
    $T->setGlobals(array('x' => 'global x'));
    $T->display($data);
    
    This code produces:
    ==================================================
    list #1, x = first
       empty
    ==================================================
    list #2, x = second
    --------------------------------------------------
      row #1 v = a, x = global x
      row #2 v = b, x = global x
    
    Now 'x' is not empty and filled with global value. But usually we need to pass variable from parent iteration to children. This is switched off by default but you can enable this by 'blitz.scope_lookup_limit' setting:
    ini_set('blitz.scope_lookup_limit', 1);
    
    This setting defines the depth of these lookups: 1 means that only one parent lookup in parent params will be proceed when variable is not found in child params. Adding this set before display we get:
    ==================================================
    list #1, x = first
       empty
    ==================================================
    list #2, x = second
    --------------------------------------------------
      row #1 v = a, x = second
      row #2 v = b, x = second
    
    These lookups were added in 0.7.5 version and are not supported in previous versions.Lookups are proceed only when variable can't be found. This means that if you have a lot of missing variables and your 'blitz.scope_lookup_limit' value is high - you will have a lot of useless hash lookups resulting in useless CPU usage. Be carefull playing with this setting.

    4.4. Includes

    In previous section you could see an example of so called "internal method" - if(). These are statements like function calls in the template code. Blitz has several internal methods like this - and one of them is include(). Just say: {{ inlcude("some.tpl") }} or {{ include($file) }} - that's it. Works same way as PHP itself. How does variable scope work with include? Right the same way as if the include statement was replaced by the content from the included file. Let's see the example. Included template represents the list of names:
    Cast: {{ BEGIN cast }}{{ UNLESS $_first }}, {{ END }}{{ $name }}{{ END }}
    
    The script sets iteration data structure for the blocks from the included template
    
    $T = new Blitz();
    $T->load('{{ include("include_ctx.tpl") }}');
    
    $data = array(
        array('name' => 'Jeff Bridges'),
        array('name' => 'John Goodman'),
        array('name' => 'Julianne Moore')
        array('name' => 'Steve Buscemi')
    );
    
    $T->set(array('cast' => $data));
    
    $T->display();
    
    This result is:
    Cast: Jeff Bridges, John Goodman, Julianne Moore, Steve Buscemi
    
    Everything is very simple. There's one tricky thing with includes though. This affects cases when you iterate included blocks using block() or iterate() methods. In this case you provide context path and Blitz checks internal template structure if this path is correct. But Blitz checks parent template, and includes are processed on the execution stage. This all happens for performance reasons. Include could be just put into block and not executed at all. In the worstest case included file name could be passed throught parameters and Blitz just can't know included block names anyway. So at the check stage Blitz can't see blocks from included template and doesn't iterate anything. Unfortunately this happens silently with no warning. This gonna be redesigned - but we have this still. To fix this just use additional third parameter in block() or iterate(), which means "just do what I say and don't check anything". See the following example. We use the same template as in prevoius example:
    Cast: {{ BEGIN cast }}{{ UNLESS $_first }}, {{ END }}{{ $name }}{{ END }}
    
    But now we use block() method
    
    $T = new Blitz();
    $T->load('{{ include("include_ctx.tpl") }}');
    
    $list_names = array('Jeff Bridges', 'John Goodman', 'Julianne Moore', 'Steve Buscemi');
    foreach ($list_names as $i_name) {
        $T->block('/cast', array('name' => $i_name), TRUE);
    }
    
    $T->display();
    
    With third parameter of block() we have the same result:
    Cast: Jeff Bridges, John Goodman, Julianne Moore, Steve Buscemi
    
    Without TRUE third parameter we'll have no iterations:
    Cast: 
    

    4.5. Debugging

    Internal representation of a template stage in Blitz is just a complex data structure with key names equal to variable or block names. This data is saved in template internally and can be dumped using method getIterations() when debugging. In the next example we iterate blocks sequentially using block() method, set some additional data with set() method, get the iteration data with getIterations() method, clean all the iterations and display the template using previously dumpled data:
    $T = new Blitz();
    $T->load('{{ include("test.tpl") }}');
    
    $list_names = array('Jeff Bridges', 'John Goodman', 'Julianne Moore', 'Steve Buscemi');
    foreach ($list_names as $i_name) {
        $T->block('/cast', array('name' => $i_name), TRUE);
    }
    
    $T->set(array('film' => 'The Big Lebowski'));
    $data = $T->getIterations();
    
    var_dump($data);
    
    $T->clean();
    var_dump($T->getIterations());
    
    $T->display($data);
    
    The result is:
    array(1) {
      [0]=>
      array(2) {
        ["cast"]=>
        array(4) {
          [0]=>
          array(1) {
            ["name"]=>
            string(12) "Jeff Bridges"
          }
          [1]=>
          array(1) {
            ["name"]=>
            string(12) "John Goodman"
          }
          [2]=>
          array(1) {
            ["name"]=>
            string(14) "Julianne Moore"
          }
          [3]=>
          array(1) {
            ["name"]=>
            string(13) "Steve Buscemi"
          }
        }
        ["film"]=>
        string(16) "The Big Lebowski"
      }
    }
    array(0) {
    }
    The Big Lebowski cast: Jeff Bridges, John Goodman, Julianne Moore, Steve Buscemi
    

    Sometimes you have correct iterations but your code doesn't work properly still. Use getStruct()/dumpStruct() methods to check if there is everything OK with your template. See View API for examples.

    4.6. Fetch

    When working with large templates you oftenly want to get a part of template. This is done by fetch() method:
    $body =<<<BODY
    {{ BEGIN test }}
    Hello, {{ $name }}!
    {{ END }}
    BODY;
    
    $T = new Blitz();
    $T->load($body);
    echo $T->fetch('test', array('name' => 'world!'));
    
    
    This code will output the contents of executed 'test' block:
    Hello, world!
    
    Fetch can be used in template body as well. Usually it's just a trick, but this works:
    $body =<<<BODY
    {{ fetch('/test') }}
    {{ BEGIN test }}Hello, world!{{ END }}
    BODY;
    
    $T = new Blitz();
    $T->load($body);
    $T->display();
    
    This code outputs "Hello, world!" as well.

    4.7. Other internal methods

    Basic internal methods are: if(), include(), date() and escape(). Two first methods were mentioned above, and two last are new. Let's cover them briefly.

    Method date(format, argument) used to format date/time values just from your template. It works as follows: when argument is integer, date() treats it as UNIX timestamp. Otherwise it's parsed with internal PHP function "php_parse_date" which recognize a lot of date formats. When ARG is omitted - the current time is used. Format string has the same conversion specifiers as PHP function "strftime".

    Method escape(string) is equal to htmlspecialchars(string, ENT_COMPAT). You can use additional second parameter to specify quoting style which is passed as string, named as the corresponding PHP constant: escape(string, "ENT_QUOTES") or escape(string, "ENT_NOQUOTES") etc. Blitz supports all three quote styles: "ENT_COMPAT", "ENT_QUOTES" and "ENT_NOQUOTES".

    5. Callbacks and plugins

    Blitz supports callbacks like {{ Some::doSomething($params) }} or {{ doSomething($param) }}. With this you can add any function calls to your template.

    Basically you can:
    1) extend Blitz and use your custom template methods in the template code
    2) call any other PHP function like do($something) or Plugin::do($something)

    Callbacks were added in 0.7.1.5 but changed in older versions. Current callback API (writing this for 0.7.1.14) works this way:

    • {{ this::method }} calls class method through the template object
    • {{ php::method }} calls PHP function "method"
    • {{ helper::method }} calls static PHP-method
    • {{ method }} calls class method first and PHP-method after (if not found) and vice versa - according to blitz.php_callbacks_first setting. This was added in 0.7.1.14 and by default it's 1 so PHP-method is called first. This can be changed in next versions according to the community reaction.
    See the following example:

    fisher@fisher:~/prj/blitz/tests> cat calls.php
    <?
    
    ini_set("blitz.php_callbacks_first", 1);
    
    $body = <<<BODY
       {{ php::date("Y/m/d H:i") }}
       self-call: {{ this::doSomething() }}
       php-call: {{ php::doSomething() }}
       this+php call: {{ doSomething() }}
       this+php call: {{ doSomething2() }}
    
    BODY;
    
    function doSomething() {
       return 'PHP :: did comething';
    }
    
    class View extends Blitz {
       function doSomething() {
           return 'THIS :: did something';
       }
    
       function __call($a, $b) {
           return '__call handler: '.var_export($a, true).', '.
                str_replace(array("\n","\r"), "", var_export($b, true));
       }
    }
    
    $T = new View();
    $T->load($body);
    $T->display();
    
    ?>
    fisher@fisher:~/prj/blitz/tests> php5 calls.php
       2010/04/02 23:57
       this-call: THIS :: did something
       php-call: PHP :: did comething
       this+php call: PHP :: did comething
       this+php call: __call handler: 'dosomething2', array ()
    
    To understand how this all works let's see how Blitz examines the call:
    • When there is a namespace and a '::' before function, Blitz checks if the namespace is a "hint". "Hints" are reserved namespaces: {{ php::do($params) }}, or {{ this::do($params) }}
      • When namespace is a "hint" ("php" or "this"). Having {{ php::do($params) }} Blitz makes a static php-function call, having {{ this::do($param) }} do() method is called through the current template object.
      • When namespace is not a "hint", Blitz makes a static PHP-call namespace::do($params). If this namespace and function exists - the function will be called.
    • When there was just a function call with no namespace like {{ do($params) }} Blitz works according to priority settings. When "blitz.php_callbacks_first" is set to 1 Blitz first tries to make a static PHP-function call. If this function doesn't exist - Blitz calls this method through your template controller. When "blitz.php_callbacks_first" is set to 0, vice versa.

    Now we can go back to the example:
    • {{ php::date("Y/m/d H:i") }} - is a PHP-function call
    • {{ this::doSomething() }} - is a "hinted" doSomething() method call through the View object $T
    • {{ php::doSomething() }} - is a "hinted" doSomething() function call from the current function scope
    • {{ doSomething() }} - is a function call from the current function scope because blitz.php_callbacks_first was set to 1
    • {{ doSomething2() }} - Blitz tries to find doSomething2() function in the current scope because blitz.php_callbacks_first was set to 1. There's no such function, so then Blitz makes a method call through the View object $T. This call is handled by __call() because doSomething2() method doesn't exist.

    6. Performance notes

    6.1. Benchmarks

    Unfortunately, I don't know any simple, universal and really correct method to analyse template engine performance. The only one right way to do this is to build your application using several template engines and measure the performance under the real conditions. Should it be noted that nobody is going to do this? That's why we always deal with artificial tests which results should be used very carefully. Nevertheless, results of one "synthetic" test are listed here.

    This test is much closer to real world conditions than most of tests you can find in the Internet (like "wow, we have printed this variable 200 times!!!11"). First we have a page of several non-trivial parts. The code is written using different template engines. This page is built by a webserver, and the performance is benchmarked using the standard apache ab utility. The page simulates a some pseudo portal main page and contains:
    • adverts (3 items)
    • news list (5 items, 5 vars each)
    • striped navigation (7 sections)
    • list of users online(~20 items)
    • vote with 3 possible answers
    • other variables (~10 items)

    Tests include lot of different template engines. Leaders are:
    • PHP mess: PHP and HTML are messed in a single file. This method is used in the real big projects in very specific cases only, but included into the tests just because it is obviously the fastest one.
    • blitz: single template, every functional block has it's own context
    • PHP includes: PHP and HTML are messed but functionally different blocks(list elements) are separated and included from the main code.
    • php_templates: single template, every functional block has it's own context
    • smarty: single template, compiled, output cache is off

    The results of this test can be found below.


    Software versions and settings were:
    PHP 4.3.10, ZPS 4.0.2
    Sigma 1.1.5 (cache on)
    Smarty 2.6.15 (tpl-compile on, output-cache off)
    Blitz 0.4.3
    FastTemplate 1.1.0
    XTemplate 0.3.0
    php_templates 1.7
    cTemplate 0.8 (ctemplate 0.4, nothreads)
    

    These measurements were made quite long time ago (PHP 4.3.* with Zend Performance Suite, and old versions of template engines). Now we need to make the same test with modern PHP 5.3.* with APC. If you can help with this - please drop me an email. I still need benchmark tests for some popular systems like CTPP, ClearSilver, Twig, Lapa, Quicky, Macro/WACT or possibly many others. If you just send me how to implement the test with these systems - I'll test it and add the results here. The benchmark code is available to download and one can add benchmark code for any other template engine.

    Results above should be interpreted in a very generalized way. Native PHP-code is obviuosly the fastest. What is more important, "native" here means written by hands, not "compiled". If you ever looked into a "compiled" template you probably know that its code has a lot of nested constructions, quite hard to execute (with a lot of unnecessary hash lookups, for instance), much more complex than written by right hands code ;) One more important thing is obvious as well: the less files you have the most effective your code is, and Blitz context methods are faster than include. I used single file methods for both php_templates and Smarty to squeeze maximum from them, so i suppose Blitz to be visibly faster.

    The difference between Blitz and commonly used "php includes" method is not very big. In real world application any difference will be likely hidden against the background of other code especially database requests et cetera. Thus both of the methods could be considered as practically equal. You can notice that most of the tests were made using an accelerator from ZPS. Using accelerator is very significant for PHP code - but you possibly can obtain different results using other products like APC/XCache/eAccelerator/whatever. In general, one should not to rely on the quoted results entirely. Make tests, play with real projects and choose solutions, which give a profit under your own particular conditions.

    6.2. Slowdowns

    Blitz performance receipes are the same as for any web-applications with CPU bottleneck. Following things in Blitz may slow down your application:
    • lot of includes (all the data will seat in VFS cache almost immediately, but includes always add some kernel syscalls and need CPU to parse/initialize objects).
    • lot of hash lookups to resolve path-variables $obj.smth.blabla, performance downgrades proportionally to the product number_of_variables*average_path_length
    • lot of hash lookups back through the scope stack
    • lot of block()/iterate() calls instead of preparing set data and just calling $View->display($data)
    • lot of non-built-in Blitz methods in templates (user defined callbacks or PHP-functions)

    7. Template API

    7.1 [IF|UNLESS]/ELSEIF/ELSE statments

    It's a simple as {{ IF $a }} if-a {{ ELSEIF $b }} else-if-b {{ ELSE }} default {{ END }}. {{ UNLESS $a }} will work instead of {{ IF $a }} as well, but no "ELSEUNLESS" is provided. Simple expressions are supported too: $a >= $b, 2 < $a, $a == "bla-bla".

    The most usual example of these statements is a list which can be empty:
    {{ IF $list }}
        some html code before the list
        {{ BEGIN list }} {{ $data }} {{ END list }}
        some html code after the list
    {{ ELSE }}
        some html code for empty list
    {{ END if-list }}
    

    7.2 if(predicate, output_true [, output_false])

    If() method is a short version for predicates. If() outputs arguments according to the first argument:
    $T = new Blitz();
    $T->load("{{ $num }}. {{ $name }} {{ if($rip,'[R.I.P.]') }}");
    
    $character = array(
        array(1, 'The Dude',            0),
        array(2, 'Walter Sobchak',      0),
        array(3, 'Donny',               1), // RIP, Donny
        array(4, 'Maude Lebowski',      0),
        array(5, 'The Big Lebowski',    0),
        array(6, 'Brandt',              0),
        array(7, 'Jesus Quintana',      0),
    );
    
    foreach ($character as $i => $i_data) {
       echo $T->parse(
           array(
               'num'    => $i_data[0],
               'name'   => $i_data[1],
               'rip'    => $i_data[2]
           )
       );
    }
    
    This code outputs:
    1. The Dude
    2. Walter Sobchak
    3. Donny [R.I.P.]
    4. Maude Lebowski
    5. The Big Lebowski
    6. Brandt
    7. Jesus Quintana
    

    7.3 include(filename)

    Include() method includes external template.

    1.tpl:
    {{ $what }}
    
    php code:
    $T = new Blitz();
    $T->load("Where is the {{ include('1.tpl') }}, Lebowski?\n");
    $T->display(array('what' => 'money'));
    
    output:
    Where's the money, Lebowski?
    

    7.4 escape(html [, "ENT_QUOTES"|"ENT_NOQUOTES"|"ENT_COMPAT")

    Escape() is a very simple html output wrapper, works like htmlspecialchars() function in PHP. There is a short alias: q(). Without second argument escape uses ENT_QUOTES as default and escapes both </> and both single and double quotes:
    $T = new Blitz();
    $T->load('{{ q($a); }}
    {{ q($a, "ENT_COMPAT") }}
    {{ q($a, "ENT_QUOTES") }}
    {{ q($a, "ENT_NOQUOTES") }}
    ');
    
    $T->display(array('a' => "here's a \"test\" <>\'\""));
    
    here&#039;s a &quot;test&quot; &lt;&gt;\&#039;&quot;
    here's a &quot;test&quot; &lt;&gt;\'&quot;
    here&#039;s a &quot;test&quot; &lt;&gt;\&#039;&quot;
    here's a "test" &lt;&gt;\'"
    

    7.5 date(format [, arg)

    Date() function formats a date. When "arg" is numerical, it is treated as UNIX timestamp integer. Otherwise it's parsed using internal PHP function "php_parse_date" which recognizes a lot of date formats. When "arg" is omitted - the current time is used. Format string has the same conversion specifiers as PHP function "strftime".

    Consider the following PHP code:
    $body = <<<BODY
    {{ date("%d %m %Y %H:%M:%S",\$time_num); }}
    {{ date("%d %m %Y %H:%M:%S",\$time_str); }}
    
    BODY;
    
    $T = new Blitz();
    $T->load($body);
    
    $time_num = mktime(11, 22, 33, 7, 22, 1976);
    $time_str = '1976-07-22 01:02:03';
    $T->display(array(
        'time_num' => $time_num,
        'time_str' => $time_str
    ));
    
    This code outputs:
    22 07 1976 11:22:33
    22 07 1976 01:02:03
    
    WARNING: Before 0.7.1.10 Blitz used internal PHP timelib library by default. This binding did't work properly. If you got something like "symbol lookup error: /path/lib/php/extensions/no-debug-non-zts-20090626/blitz.so: undefined symbol: timelib_time_ctor") - then the fastest way to fix this is either to get newest source or to recompile Blitz with changed line "#define BLITZ_WITH_TIMELIB 0".

    8. View API


    NOTE: depending on your religious views you may use either CamelCaps or under_bars coding style. For any doSomething Blitz can do_something as well.

    8.1. load($body)

    Load() just loads template body from variable:
    $T = new blitz();
    $T->load('Have a lot of {{ $what}}!');
    $T->display(array('what' => 'fun'));
    

    8.2. set($vars)

    Set variables or complex iteration data into the current context. Set accepts mixed $vars that represent internal iteration state: when it's array - it's interpreted as list.
    $T = new Blitz();
    $T->load("{{ BEGIN characters }} {{ \$_num }}. {{ \$name }} {{ if(\$rip,'[R.I.P.]') }}\n{{ END characters}}");
    
    $data = array(
        'characters' => array(
            array('name' => 'The Dude'),
            array('name' => 'Walter Sobchak'),
            array('name' => 'Donny', 'rip' => TRUE), // RIP, Donny
            array('name' => 'Maude Lebowski'),
            array('name' => 'The Big Lebowski'),
            array('name' => 'Brandt'),
            array('name' => 'Jesus Quintana'),
        )
    );
    
    $T->set($data);
    $T->display();
    
    This code outputs:
     1. The Dude
     2. Walter Sobchak
     3. Donny [R.I.P.]
     4. Maude Lebowski
     5. The Big Lebowski
     6. Brandt
     7. Jesus Quintana
    
    If you want to render the whole template several times - there should be an array at the most upper level:
    T = new Blitz();
    $T->load("Hello {{ \$name }}!\n");
    $T->set(
        array(
            0 => array('name' => 'dude'),
            1 => array('name' => 'world')
        )
    );
    $T->display();
    
    this code outputs:
    Hello dude!
    Hello world!
    

    8.3. parse([$vars])

    Parse() method sets $vars and returns rendered template as string. Vars structure has the same semantics as for set(). You can just say "echo $T->parse($vars)" instead of $T->set($vars) and $T->display() in previous example.

    8.4. display([$vars])

    Display() method sets $vars and outputs rendered template. Vars structure has the same semantics as for set(). You can just say $T->display($vars) instead of $T->set($vars) and $T->display() in previous example.

    8.5. block($path, $vars [, $iterate_nonexistant = FALSE])

    Block() populates context $path with $vars. When $iterate_nonexistant is TRUE block() will not check if this $path is valid (this may be useful for complex templates with includes). main.tpl:
    {{ $question }}
    {{ BEGIN answers }}
    - {{ $text }}
    {{ END}}
    
    php code:
    $T = new Blitz('ex3.tpl');
    $T->display(
        array(
            'question' => 'Do I make myself clear, Lebowski?',
            'answers' => array(
                0 => array('text' => 'Yes'),
                1 => array('text' => 'No'),
                2 => array('text' => 'Sorry, I wasn\'t listening'),
            )
        )
    );
    
    this code outputs:
    Do I make myself clear, Lebowski?
    - Yes
    - No
    - Sorry, I wasn't listening
    

    8.6. fetch($path, $vars)

    Fetch() method outputs context $path rendered with $vars. Example

    8.7. include($filename, $vars)

    In some cases you need to include a template from your PHP-code. This happens quite rarely and basically it's a bad style cause this increases template controller/code coupling, but if you need to do this without creating Blitz instance - you can use include() method. Consider this php code:
    class View extends Blitz {
        var $news = array();
    
        function View($tmpl_name) {
            return parent::Blitz($tmpl_name);   
        }
        
        function setNews($data) {
            $this->news = $data;
        }
        
        function showNewsList() {
            $result = '';
            foreach ($this->news as $i_news) {
                $result .= $this->include('news_list_item.tpl', $i_news);
            }
            return $result;
        }
    }
    
    Here showNewsList() method gets the result of news_list_item.tpl template execution, passing news data into included template. Blitz supports context iteration when you include template with contexts:
    $T = new Blitz();
    $T->load('{{ include("include_ctx.tpl") }}');
    
    $i = 0;
    while ($i<5) {
        $T->block('/test1', array('var' => $i), TRUE);
        $T->block('/test2', array('var' => $i + 1), TRUE);
        $i++;
    }
    
    echo $T->parse();
    
    include_ctx.tpl:
    test1: {{ begin test1 }} v1={{ $var }} {{ end }}
    test2: {{ begin test2 }} v2={{ $var }} {{ end }}
    
    output:
    test1:  v1=0  v1=1  v1=2  v1=3  v1=4 
    test2:  v2=1  v2=2  v2=3  v2=4  v2=5
    
    Note that we used an additional 3rd parameter in block method. This parameter tells blitz to iterate /test1 and /test2 blocks without any check if corresponding context exists. Include is processed at the execution stage, that's why Blitz knows nothing about included templates structure when setting varibles or iterating contexts.

    8.8. clean(), cleanGlobals()

    Clean() method just cleans-up all template iterations and sets the template in the initial stage as if it was just created:
    $T = new Blitz();
    $T->load('{{ IF question }}
    {{ BEGIN question }}Who the fuck are the {{ $name }}? {{ END question }}
    {{ ELSE }}
    empty
    {{ END }}');
    
    $T->set(array('question' => array('name' => 'Knutsens')));
    $T->display();
    
    var_dump($T->getIterations());
    
    $T->clean();
    var_dump($T->getIterations());
    
    $T->display();
    
    This code outputs:
    Who the fuck are the Knutsens?
    array(1) {
      [0]=>
      array(1) {
        ["question"]=>
        array(1) {
          ["name"]=>
          string(8) "Knutsens"
        }
      }
    }
    array(0) {
    }
    empty
    

    CleanGlobals() is the same but cleans globals, not iteration data.

    8.9. setGlobals($global_vars), getGlobals()

    getGlobals() returns globals hash array, setGlobals($global_vars) - adds $global_vars to the global variables hash:
    $body = '
    It\'s like what {{ $local }} said...
    It\'s like what {{ $global }} said...
    {{ BEGIN context }}
        It\'s like what {{ $local }} said...
        It\'s like what {{ $global }} said...
    {{ END context }}
    ';
    
    $T = new Blitz();
    $T->load($body);
    $T->set(array('local' => 'Lennon'));
    $T->setGlobals(array('global' => 'Lenin'));
    $T->block('context', array('local' => 'Lenin'));
    $T->display();
    var_dump($T->getGlobals());
    var_dump($T->getIterations());
    
    This code outputs:
    It's like what Lennon said...
    It's like what Lenin said...
        It's like what Lenin said...
        It's like what Lenin said...
    array(1) {
      ["global"]=>
      string(5) "Lenin"
    }
    array(1) {
      [0]=>
      array(2) {
        ["local"]=>
        string(6) "Lennon"
        ["context"]=>
        array(1) {
          [0]=>
          array(1) {
            ["local"]=>
            string(5) "Lenin"
          }
        }
      }
    }
    

    Please remember that usually you don't need to pass local variables to every block - you can just use "scope lookups" with blitz.scope_lookup_limit and varibles from parent blocks will be visible in inner blocks. Here $local is passed to "/context" just because we needed this variable to have different values in the root "/" and "/context".

    8.12. hasContext($path)

    This method just checks if the context exists or not and returns TRUE/FALSE correspondingly.
    $body = '{{ BEGIN walter }} I don\'t roll on Shabbos {{ END }}';
    
    $T = new Blitz();
    $T->load($body);
    
    var_dump($T->hasContext('walter'));
    var_dump($T->hasContext('donny'));
    
    This code outputs:
    bool(true)
    bool(false)
    

    8.13. context($path)


    8.14. iterate($path [, $iterate_nonexistant])

    8.15. getContext()


    8.16. getStruct()

    getStruct() - returns template structure as an array of available paths.

    8.17. dumpStruct()

    8.18. getIterations()

    getIterations() - get internal iteration data representing the current template state. Example