Home

Awesome

LightnCandy

⚡🍭 An extremely fast PHP implementation of handlebars ( http://handlebarsjs.com/ ) and mustache ( http://mustache.github.io/ ).

CI status: Unit testing Regression testing tested PHP: 7.1, 7.2, 7.3, 7.4, 8.0, 8.1 Coverage Status

Package on packagist: Latest Stable Version License Total Downloads

Features

Installation

Use Composer ( https://getcomposer.org/ ) to install LightnCandy:

composer require zordius/lightncandy:dev-master

UPGRADE NOTICE

Documents

Compile Options

You can apply more options by running LightnCandy::compile($template, $options):

LightnCandy::compile($template, array(
    'flags' => LightnCandy::FLAG_ERROR_LOG | LightnCandy::FLAG_STANDALONEPHP
));

Default is to compile the template as PHP, which can be run as fast as possible (flags = <a href="https://zordius.github.io/HandlebarsCookbook/LC-FLAG_BESTPERFORMANCE.html">FLAG_BESTPERFORMANCE</a>).

Error Handling

JavaScript Compatibility

Mustache Compatibility

Handlebars Compatibility

Handlebars Options

PHP

Partial Support

Custom Helper

Custom Helper Examples

#mywith (context change)

// LightnCandy sample, #mywith works same with #with
$php = LightnCandy::compile($template, array(
    'flags' => LightnCandy::FLAG_HANDLEBARSJS,
    'helpers' => array(
        'mywith' => function ($context, $options) {
            return $options['fn']($context);
        }
    )
));
// Handlebars.js sample, #mywith works same with #with
Handlebars.registerHelper('mywith', function(context, options) {
    return options.fn(context);
});

#myeach (context change)

// LightnCandy sample, #myeach works same with #each
$php = LightnCandy::compile($template, array(
    'flags' => LightnCandy::FLAG_HANDLEBARSJS,
    'helpers' => array(
        'myeach' => function ($context, $options) {
            $ret = '';
            foreach ($context as $cx) {
                $ret .= $options['fn']($cx);
            }
            return $ret;
        }
    )
));
// Handlebars.js sample, #myeach works same with #each
Handlebars.registerHelper('myeach', function(context, options) {
    var ret = '', i, j = context.length;
    for (i = 0; i < j; i++) {
        ret = ret + options.fn(context[i]);
    }
    return ret;
});

#myif (no context change)

// LightnCandy sample, #myif works same with #if
$php = LightnCandy::compile($template, array(
    'flags' => LightnCandy::FLAG_HANDLEBARSJS,
    'helpers' => array(
        'myif' => function ($conditional, $options) {
            if ($conditional) {
                return $options['fn']();
            } else {
                return $options['inverse']();
            }
        }
    )
));
// Handlebars.js sample, #myif works same with #if
Handlebars.registerHelper('myif', function(conditional, options) {
    if (conditional) {
        return options.fn(this);
    } else {
        return options.inverse(this);
    }
});

You can use isset($options['fn']) to detect your custom helper is a block or not; you can also use isset($options['inverse']) to detect the existence of {{else}}.

Data variables and context

You can get special data variables from $options['data']. Using $options['_this'] to receive current context.

$php = LightnCandy::compile($template, array(
    'flags' => LightnCandy::FLAG_HANDLEBARSJS,
    'helpers' => array(
        'getRoot' => function ($options) {
            print_r($options['_this']); // dump current context
            return $options['data']['root']; // same as {{@root}}
        }
    )
));
Handlebars.registerHelper('getRoot', function(options) {
    console.log(this); // dump current context
    return options.data.root; // same as {{@root}}
});

Private variables

You can inject private variables into inner block when you execute child block with second parameter. The example code showed similar behavior with {{#each}} which sets index for child block and can be accessed with {{@index}}.

$php = LightnCandy::compile($template, array(
    'flags' => LightnCandy::FLAG_HANDLEBARSJS,
    'helpers' => array(
        'list' => function ($context, $options) {
            $out = '';
            $data = $options['data'];

            foreach ($context as $idx => $cx) {
                $data['index'] = $idx;
                $out .= $options['fn']($cx, array('data' => $data));
            }

            return $out;
        }
    )
));
Handlebars.registerHelper('list', function(context, options) {
  var out = '';
  var data = options.data ? Handlebars.createFrame(options.data) : undefined;

  for (var i=0; i<context.length; i++) {
    if (data) {
      data.index = i;
    }
    out += options.fn(context[i], {data: data});
  }
  return out;
});

Change Delimiters

You may change delimiters from {{ and }} to other strings. In the template, you can use {{=<% %>=}} to change delimiters to <% and %> , but the change will not affect included partials.

If you want to change default delimiters for a template and all included partials, you may compile() it with delimiters option:

LightnCandy::compile('I wanna use <% foo %> as delimiters!', array(
    'delimiters' => array('<%', '%>')
));

Template Debugging

When template error happened, LightnCandy::compile() will return false. You may compile with FLAG_ERROR_LOG to see more error message, or compile with FLAG_ERROR_EXCEPTION to catch the exception.

You may generate debug version of templates with FLAG_RENDER_DEBUG when compile() . The debug template contained more debug information and slower (TBD: performance result) , you may pass extra LightnCandy\Runtime options into render function to know more rendering error (missing data). For example:

$template = "Hello! {{name}} is {{gender}}.
Test1: {{@root.name}}
Test2: {{@root.gender}}
Test3: {{../test3}}
Test4: {{../../test4}}
Test5: {{../../.}}
Test6: {{../../[test'6]}}
{{#each .}}
each Value: {{.}}
{{/each}}
{{#.}}
section Value: {{.}}
{{/.}}
{{#if .}}IF OK!{{/if}}
{{#unless .}}Unless not OK!{{/unless}}
";

// compile to debug version
$phpStr = LightnCandy::compile($template, array(
    'flags' => LightnCandy::FLAG_RENDER_DEBUG | LightnCandy::FLAG_HANDLEBARSJS
));

// Save the compiled PHP code into a php file
file_put_contents('render.php', '<?php ' . $phpStr . '?>');

// Get the render function from the php file
$renderer = include('render.php');

// error_log() when missing data:
//   LightnCandy\Runtime: [gender] is not exist
//   LightnCandy\Runtime: ../[test] is not exist
$renderer(array('name' => 'John'), array('debug' => LightnCandy\Runtime::DEBUG_ERROR_LOG));

// Output visual debug template with ANSI color:
echo $renderer(array('name' => 'John'), array('debug' => LightnCandy\Runtime::DEBUG_TAGS_ANSI));

// Output debug template with HTML comments:
echo $renderer(array('name' => 'John'), array('debug' => LightnCandy\Runtime::DEBUG_TAGS_HTML));

The ANSI output will be:

<a href="tests/example_debug.php"><img src="https://github.com/zordius/lightncandy/raw/master/example_debug.png"/></a>

Here are the list of LightnCandy\Runtime debug options for render function:

Preprocess Partials

If you want to do extra process before the partial be compiled, you may use prepartial when compile(). For example, this sample adds HTML comments to identify the partial by the name:

$php = LightnCandy::compile($template, array(
    'flags' => LightnCandy::FLAG_HANDLEBARSJS,
    'prepartial' => function ($context, $template, $name) {
        return "<!-- partial start: $name -->$template<!-- partial end: $name -->";
    }
));

You may also extend <a href="https://zordius.github.io/lightncandy/class-LightnCandy.Partial.html">LightnCandy\Partial</a> by override the <a href="https://zordius.github.io/lightncandy/class-LightnCandy.Partial.html#_prePartial">prePartial()</a> static method to turn your preprocess into a built-in feature.

Customize Render Function

If you want to do extra tasks inside render function or add more comment, you may use renderex when compile() . For example, this sample embed the compile time comment into the template:

$php = LightnCandy::compile($template, array(
    'flags' => LightnCandy::FLAG_HANDLEBARSJS,
    'renderex' => '// Compiled at ' . date('Y-m-d h:i:s')
));

Your render function will be:

function ($in) {
    $cx = array(...);
    // compiled at 1999-12-31 00:00:00
    return .....
}

Please make sure the passed in renderex is valid PHP, LightnCandy will not check it.

Customize Rendering Runtime Class

If you want to extend LightnCandy\Runtime class and replace the default runtime library, you may use runtime when compile() . For example, this sample will generate render function based on your extended MyRunTime:

// Customized runtime library to debug {{{foo}}}
class MyRunTime extends LightnCandy\Runtime {
    public static function raw($cx, $v) {
        return '[[DEBUG:raw()=>' . var_export($v, true) . ']]';
    }
}

// Use MyRunTime as runtime library
$php = LightnCandy::compile($template, array(
    'flags' => LightnCandy::FLAG_HANDLEBARSJS,
    'runtime' => 'MyRunTime'
));

Please make sure MyRunTime exists when compile() or rendering based on your FLAG_STANDALONEPHP .

Unsupported Feature

Suggested Handlebars Template Practices

Detail Feature list

Go http://handlebarsjs.com/ to see more feature description about handlebars.js. All features align with it.

Developer Notes

Please read <a href=".github/CONTRIBUTING.md">CONTRIBUTING.md</a> for development environment setup.

Framework Integration

Tools