/text-template

Single-Class string template engine for PHP5/7/8 supporting if/loops/filters

Primary LanguagePHPMIT LicenseMIT

Text-Template (Single Class, IF, FOR, FILTERS)

Downloads this Month Latest Stable Version Actions Status Coverage Status Supports PHP 5.4+ Supports PHP 7.0+ Homepage Examples

{if user.searching=='template'}This is for you {= user.name }{else}Welcome {= user.name }{/if}

Single-Class PHP5/7 template engine with support for if/loops/filters

  • Easy: No compiling or caching - just parse input string into output string
  • Secure: No eval(); no code is generated. No filesystem access needed. Unit-tested.
  • Small: No dependencies.
  • Features: Nested loops, if/elseif/else, custom filters, auto-escaping

It is aimed to be a small string Template-Engine to meet e-Mailing or small html-Template demands. It is not meant to be a replacement for pre-compiled full featured Template-Engines like Smarty or Twig.

TextTemplate uses Regular-Expressions for text-Parsing. No code is generated or evaluated - so this might be a secure solution to use in no time-critical situations.

Whilst most template-engines rely on eval'ing generated code and filesystem-access, Text-Template uses a
set of regular-expressions to parse the template. Nor any intermediate code is generated nor any code is eval'ed. So TextTemplate should be more secure than Smarty or Twig by design.

TextTemplate supports infinite-nested loops and sequences.

Basic Example

// 1. Define the Template
$tplStr = <<<EOT

Hello World {= name }
{if name == "Matthias"}
Hallo {= name | capitalize }
{elseif name == "Jan"}
Hi Buddy
{else}
You are not Matthias
{/if}

EOT;

// 2. Define the Data for the Template
$data = [
    "name" => "Matthias"
];

// 3. Parse
$tt = new TextTemplate($tplStr);
echo $tt->apply ($data);

Install

I prefer and recommend using composer:

composer require text/template

Flexible tag-start/end

This documentation refers to the default-tags: Having one bracket { as start and one } as end delimiter. Of course, they are flexible: If you want to change these, see: TextTempate::setOpenCloseTagChars()

Value injection

Use the value Tag

{= varName}

To inject a value to the Code. Any variables will be htmlspecialchars() encoded by default. To output the RAW content use the raw-Filter: {=htmlCode|raw}

To access array elements or objects use "." to access sub-elements:

{= users.0.name}

Loops

You can insert loops:

{for curName in names}
Current Name: {= curName}
{/for}

Inside each loop, there are to magick-values @index0 (index starting with 0) and @index1 for a index starting with amp1.

{for curName in names}
Line {= @index1 }: {= curName}
{/for}

Inside loops you can {break} or {continue} the loop.

Conditions (if)

You can use if-conditions:

{if someVarName == "SomeValue"}
Hello World
{/if}

Shortcut: Test if a variable is null:

{if someVarName}
    someVarName is set!
{/if}
{if !someVarName}
    someVarName is not set!
{/if}

Complex logical expressions can be made using && (and), || (or) and brackets.

{if someVarName && otherVarName}
    someVarName and otherVarName are set!
{/if}
{if someVarName || otherVarName}
    someVarName or otherVarName are set!
{/if}
{if someVarName || (otherVarName && anotherVarName)}
    Condition is true!
{/if}
{if someVarName && !(otherVarName && anotherVarName)}
    Condition is true!
{/if}

You can use filters on values used in comparsions.

{if someArray|count > otherArray|count}
    someArray has more items than otherArray
{/if}

Adding Operators

You can add custom operators for use in conditions.

Adding a new Operator:

$tt->addOperator("contains",
    function ($operand1, $operand2) {
        return strpos($operand1, $operand2) !== false; 
    }
);

Predefined Operators

Operator Description
== Equal
!= Not equal
> Greater than
< Less than
>= Greater than or equal
<= Less than or equal

Conditions (else)

{if someVarName == "SomeValue"}
Hello World
{else}
Goodbye World
{/if}

Lists of choices:

{if someVarName == "SomeValue"}
Hello World
{elseif someVarName == "OtherValue"}
Hello Moon
{else}
Goodbye World
{/if}

Calling Functions

You can register user-defined functions.

$template->addFunction("sayHello", 
    function ($paramArr, $command, $context, $cmdParam, $self) {
        return "Hello " . $paramArr["msg"];
    }
);

Call the function and output into template

{sayHello msg="Joe"}

or inject the Result into the context for further processing:

{sayHello msg="Joe" > out}
{=out}

Processing Exceptions:

Use !> to catch exceptions and redirect them to the scope.

{throw msg="SomeMsg" !> lastErr}

Or use !break or !continue to break/continue a loop

Comments

Use {# #} to add comments (will be stripped from output

Template {# Some Comment #}
{# Some
Multiline
Comment #}

Adding Filters

You can add custom filters or overwrite own filters. The default filter is html (htmlspecialchars).

Adding a new Filter:

$tt->addFilter ("currency", function ($input, $decimals=2, $decSeparator=",", $thounsandsSeparator=".") {
    return number_format ($input, $decimals, $decSeparator, $thousandsSeparator);
});

Call the filter with parameters (parameter-separator :):

{= variable | currency:2:,:. }

Use this filter inside your template

{= someVariable | currency }

Predefined Filters

Name Description
raw Display raw data (skip default escaping)
singleLine Transform Line-breaks to spaces
inivalue like singleLine including addslashes()
html htmlspecialchars()
fixedLength::<pad_char: Pad / shrink the output to characters
inflect:tag Convert to underline tag
sanitize:hostname Convert to hostname
count Return count of array

Replacing the default-Filter

By default and for security reason all values will be escaped using the "DEFAULT"-Filter. (except if "raw" was selected within the filter section)

If you, for some reason, want to disable this functionality or change the escape function you can overwrite the DEFAULT-Filter:

$tt->addFilter ("_DEFAULT_", function ($input) {
    return strip_tags ($input);
});

or

$tt->setDefaultFilter("singleLine");

This example will replace the htmlspecialchars() escaper by the strip_tags() function.

Sections

Sections are like functions but provide the content they enclose:

{sectionxy name="someName"}
Some Content
{/sectionxy}
{sectionxy name="someName" > out}
Some Content
{/sectionxy}

{= out}

To use sections you must just set the callback:

$textTemplate->addSection("sectionxy", function ($content, $params, $command, $context, $cmdParam, $self) {
    return "Content to replace section content with";
});

Stripping empty lines

{strip_empty_lines}
line1

line2
{/strip_empty_lines}

Counting a array

{trim > countElem}{= var | count}{/trim}
{if countElem == "0" }
{/if}

Function return redirection

Append output to a variable.

{print >> out}
A
{/print}
{print >> out}
B
{/print}

{= out}

Debugging the Parameters

To see all Parameters passed to the template use:

{= __CONTEXT__ | raw}

It will output the structure of the current context.

Dealing with undefined Variables

By default text-template will not throw any exceptions when a template tries to access an undefined variable.

To improve debugging, you can switch this behaviour by setting $softFail to false (Parameter 2 of apply() function):

try {
    $tt = new TextTemplate("{=someUndefinedName}");
    echo $tt->apply([], false);
    //                  ^^^^^
} catch (UndefinedVariableExceptions $e) {
    echo "UndefinedVariable: {$e->getTriggerVarName()}"
}

will return

UndefinedVariable: someUndefinedName

Changing the tag-open and tag-close chars

Sometimes {tag}{\tag} isn't suitable when parsting other template files. You can change the opening and closing chars with the function setOpenCloseTagChars()

$textTemplate->setOpenCloseTagChars("{{", "}}");

The above example will listen to {{tag}}{{/tag}}.

Benchmark

Although the parser is build of pure regular-expressions, I tried to avoid too expensive constructions like read ahead, etc.

And we got quite good results:

Template size Parsing time[sec]
50kB 0.002
200kB 0.007

Contributing, Bug-Reports, Enhancements

If you want to contribute, please send your Pull-Request or open a github issue.

Keeping the tests green: Please see / provide unit-tests. This project uses nette/tester for unit-testing.

This Project uses kickstart's ready to use development containers based on docker. Just run ./kickstart.sh to run this project.

To start the development container

./kickstart.sh

To execute the tests run kick test inside the container. (See .kick.yml)

About

Text-Template was written by Matthias Leuffen http://leuffen.de

Join InfraCAMP