Dust PHP is a PHP implementation of LinkedIn's Dust (fork) template engine written in JavaScript. Dust PHP is written in Pratphall. It was built while searching for a templating language that was more powerful than mustache, concise, built in JavaScript, and contained idioms easily translatable to PHP. This is the result. It is MIT licensed. Checkout the code, submit issues, and send pull requests on the GitHub page. PHP 5.4 is required to use Dust PHP.
The rest of this manual is based on the Dust tutorial from LinkedIn and uses examples/text from it. The LinkedIn tutorial is recommended reading to understand the JavaScript side and how similar the PHP side is.
Here is an example of a template, a context, and the output:
{title}
<ul>
{#names}
<li>{name}</li>{~n}
{/names}
</ul>
$context = [
'title' => 'Famous People',
'names': [
['name' => 'Larry'],
['name' => 'Curly'],
['name' => 'Moe']
]
];
$context = (object)[
'title' => 'Famous People',
'names': [
(object)['name' => 'Larry'],
(object)['name' => 'Curly'],
(object)['name' => 'Moe']
]
];
$context = json_encode('{
"title": "Famous People",
"names": [
{ "name": "Larry" },
{ "name": "Curly" },
{ "name": "Moe" }
]
}');
class Name
{
public $name;
public function __construct($name)
{
$this->name = $name;
}
}
class Context
{
public $title = 'Famous People';
public function names()
{
return [new Name('Larry'), new Name('Curly'), new Name('Moe')];
}
}
$context = new Context();
Output:
Famous People
<ul>
<li>Larry</li>
<li>Curly</li>
<li>Moe</li>
</ul>
Dust templates output plain old text and processes dust tags -- {xxxxxx}
being a Dust tag format. The tag structure is similar to html in general form but using braces instead of <>, e.g. {name /}
, {name}body inside tag{/name}
and with parameters {name param="val1" param2="val",... }
.
The simplest form is just
{name}
and is called a key. It references a value from the data named "name". In our example, you saw the key {title}
which produced an output value of "Famous People". The other tag form we saw in the example was
{#names}....{/names}
This is called a section.
{name}
key in the section body.
{object.propName}
.
{! Comment syntax !}
is how you write comments{name|s}
suppresses auto-escaping{name|h}
force HTML escaping{name|j}
force JavaScript escaping{name|u}
encodes with same result as JS's encodeURI{name|uc}
encodes with same result as JS's encodeURIComponent{name|js}
stringify JSON literal{name|jp}
parse JSON string to object{name|s|h}
{~n}
- newline, {~r}
- CR, {~lb}
- left bracket, {~rb}
- right bracket, {~s}
- space
To install, simply require dust-php/dust-php in your composer.json like so:
{
"require": {
"dust-php/dust-php": "0.1.0"
}
}
Then it can be used in the project very easily. You can also download a source release here.
Dust has a very simple API that makes it easy to render templates. The following creates a new Dust PHP parser/renderer and runs a simple template:
//create object
$dust = new \Dust\Dust();
//compile a template
$template = $dust->compile('Strings: {#strings}{.}{@sep},{/sep}{/strings}');
//render the template
$output = $dust->renderTemplate($template, ['strings' => ['a', 'b', 'c']]);
//echo the output
echo($output);
As expected, that will output Strings: a,b,c
. You can also compile the template to a name and render it later with that name. This is helpful for partials (explained later). For example:
//create object
$dust = new \Dust\Dust();
//compile a template to a name
$dust->compile('Strings: {#strings}{.}{@sep},{/sep}{/strings}', 'myTemplate');
//render the template for that name
$output = $dust->render('myTemplate', ['strings' => ['a', 'b', 'c']]);
//echo the output
echo($output);
Compiling a file will allow you to reference other files as partials and blocks relative to the directory of the file:
//create object
$dust = new \Dust\Dust();
//compile a template to a name
$template = $dust->compileFile('templates/myTemplate.dust');
//render the template for that name
$output = $dust->renderTemplate($template, ['strings' => ['a', 'b', 'c']]);
//echo the output
echo($output);
Any exceptions that occur on compiling or rendering are extensions of \Dust\DustException
. The Dust object contains a few public properties that can be retrieved or modified:
templates
- An associative array containing the compiled template AST's, keyed by the template name or absolute file pathfilters
- An associative array containing all of the available filters keyed by the filter namehelpers
- An associative array containing all of the available helpers keyed by the helper nameautomaticFilters
- An indexed array containing all filters the are applied by default to all textparser
- Reference to the parser used for compilingevaluator
- Reference to the evaluator used for renderingincludedDirectories
- An ordered, indexed array of directories to load partials/blocks from (default empty)autoloaderOverride
- An optional overload that should be a callable accepting a string and returning an AST or nullAll returned parsed templates may be serialized and deserialized easily with normal PHP serialization.
Dust gets its data values from the PHP object or array used to render the template. There are main types of data: scalars, arrays, and objects. The data can also be a function but we are not considering that case here.
Referencing a scalar value is done with a simple key, e.g. {name}. Individual array elements can be referenced by subscripting, e.g. array[3]. Object properties or associative arrays are referenced using paths, e.g name.firstName. Of course, a path reference can be to a scalar or an array which could then be subscripted.
A section is a Dust tag of the form {#names}...{/names}
. It is the way you loop over data in Dust. What happens is the current context (more on context shortly) is set to the "names" part of your data. If names is an array, the body wrapped by the section tag is executed for each element of the array. If the element is not an array, the body will just be executed once. If the "names" element does not exist or is empty, the body will be skipped.
During the execution of the section iteration, three variables are defined: $idx - the index of the current iteration, $len - the number of elements in the data being iterated, and $iter - the current number of values in to the iteration. For numerically indexed arrays, $idx and $iter will be the same, but for associative arrays $idx is the key whereas $iter is the position.
So if there are two instances w/ a name property in my data, how does Dust decide which one to use to render {name}
?
{#name}....{/name}
Dust sets its context to the portion of the model identified by name. When you first start rendering, the context is set to the outermost level of the object. Thus the {#names}
section positions the context to the block the names part, which happens to be an array. Therefore, the {name}
key in the section body is matched against the one in the context "names".
Let's explore how context works with a more complex model (this is an associative array, but could be an class instance or a dynamic stdClass and it would behave the same way).
[
'name' => 'root',
'anotherName' => 'root2',
'A' => [
'name' => 'Albert',
'B' => [
'name' => 'Bob'
]
]
]
As we learned earlier, if you have {#A}{name}{/A}
the current context is A and everything under it (i.e. it includes the B stuff). The key for {name}
will output "Albert" because that is the direct value of name in the context of "A".
So how does it work if you have {#A}{anotherName}{/A}
? You will get "root2" as the output. That's because "anotherName" could not be found directly under "A" so Dust tries to walk up to the parent of "A" (which is the root context in our case) and finds "anotherName" hence using its value. In general, a simple key reference will look first in the current context and, if not found, search all higher levels up to the root looking for the name. It will not search downward into things like "B" that are nested within "A".
Suppose our context is the root and say we want to work with the data "only" under "B". You can use a dotted notation called a "path" to do this. For example, {A.B.name}
will output "Bob".
Simple key references like {A.B.name}
are sometimes not enough. You might need to use a section to iterate over a sub-part of the model. Remember when you use {#xxxx}
for a section, you also establish a new context at that point. For example, in the case of {#A.B}{name}{/A.B}
, This will output "Bob" because our context has been set to B within A. Path notation only allows you to reach down to a nested context visible within the current context.
You CANNOT reference a value using paths when that pathed value is outside your current context, somewhere "above" you. Lets look at an example to make this point clear.
{#A.B}
name in B={name} name in A= {A.name}
{/A.B}
The above will output "name in B=Bob name in A=" showing that A.name is not accessible inside the context A.B.
IMPORTANT: While you cannot use a dotted path notation to reference ancestor/parents from the current context, you can use a non-pathed section reference to adjust your context to a higher point. For example,
{#A.B} name in B={name}
{#A}
name in A: {name}
{/A}
{/A.B}
After {#A.B}
our context is "B" and since we set the context by a path, we cannot reach up to {A.name}
. However, {#A}
, a non-pathed reference, is allowed to search upward and find "A". Then {#A}
sets a new context to "A" allowing us to reference the name value under "A". When the closing tag {/A}
is reached, the context reverts to {#A.B}
, In essence, the context acts like a stack.
Another way to reference a value outside your current context is to pass the value into the section as an inline parameter (we will talk more about parameters soon). Here is an example of how to access A.name within the {#A.B}
context using a parameter on the {#A.B}
section
{#A.B param=A.name}
name in B={name} name in A: {param}
{/A.B}
Normally the visibility of data from the model is controlled by your current context set by the # tag, or by inline parameters, plus the ability to access values by the key reference {name}
and to reset the current context based on a #section reference to outer block using {#outerBlock}
.
There is another way to control and limit visibility for a block of code. The notation
{#name:name2}.... {/name}
will do that.
Specifically it does the folllowing:
This prevents {key}
references from accessing any data not in the name or name2 contexts. No further reaching up can happen even with simple key forms like {name}
. This scope limitation might be useful for data hiding from components. Another use for it could be to make only the current context and it's peer available.
Given a data model where A and B are peers and we need to iterate over A and also reference data from B, without explicit context setting we would have trouble doing this. Remember, this could be an object but is just shown here as an associative array.
[
'A' => [
'names' => ['Albert', 'Alan']
],
'A2' => [
'type' => 'Student'
]
]
However, the following:
{#A:A2} {#names}{.} - {type} {/name} {/A}
will output "Albert - Student Alan - Student" since both A and A2 are on the context stack even though A2 would not normally be there.
Since we just dropped a teaser about parameters, let's look at them. Section tags allow you to pass parameters into the section for subsequent use. Parameter values can be simple string constants or the name of a value from the data model. For example, using the same data model as earlier:
{#A.B foo="Hi" bar=" Good to see you"}
{foo} {name} {bar}
{/A.B}
This will output "Hi Bob Good to see you"
As we saw earlier, values from the data model can also be passed. Consider
{#A.B foo=A.name bar=anotherName}
{foo} {name} {bar}
{/A.B}
This will output "Albert Bob root2". It's important to understand the context at the point the parameter values are established. With foo=A.name above, A.name is evaluated before the context is moved to A.B, thus A.name is accessible.
However, if the parameter values are interpolated into strings, they are evaluated in the context of the section using them. Therefore, the following will just output "B root2" because {A.name}
is not accessible from the {#A.B}
context.
{#A.B foo="{A.name}" bar="{anotherName}" }
{foo} {name} {bar}
{/A.B}
While you can specify an object as a parameter, e.g.
{#A.B foo=A }
{foo.name}
{/A.B}
you cannot do anything useful with it since the {foo.name}
reference is going to look for foo in the current context but that context is the element of the current iteration of the section #A.B (in this case just the name: "Bob", value). Therefore, "foo" won't be found. The foo parameter is on the context stack but one level higher than the current element iteration so unreachable by a path reference.
When deciding on parameter names, try to be unique. Inline parameters will not override the current context if a property of the same name exists. Let's look at an example:
{#A name="Not Albert"}
name is {name}.
{/A}
will output "name is Albert" since preference goes to data in the current context followed by inline parameters then up the context tree.
If we want to be sure we get the value in the parameter we can make it unique.
{#A paramName="Not Albert"}
name is {paramName} and {B.name} is still Bob.
{/A}
will output "name is Not Albert and Bob is still Bob".
Templates with logic versus "logic-less" templates is a hotly debated point among template language designer and users. Dust straddles the divide by adopting a "less logic" stance. We all know that real business logic does not belong in the presentation layer, but what about simple presentation-oriented things like coloring alternate rows in table or marking the selected option in a <select> dropdown? It seems equally wrong to ask the controller/business logic code to compute these down to simple booleans in order to reduce the logic in the presentation template. This route just leads to polluting the business layer code with presentation-oriented logic.
Dust provides some simple logic mechanisms and trusts you to be sensible in minimizing the logic in your templates to only deal with presentation-oriented decisions. That said, let's take a look at the ways Dust lets you have logic.
There are two other special section notations that provide conditional testing:
{?name} body {/name}
{^name} body {/name}
Note that the value of name is evaluated as follows: "" or ' ' will evaluate to false, boolean false, null, or undefined will evaluate to false, numeric 0 evaluates to true, so does, string "0", string "null", string "undefined" and string "false". Also note that empty array (i.e. []) is evaluated to false and empty object and non-empty object are evaluated to true.
Here is an example of doing something special when the array is empty.
Template:
<ul>
{#friends}
<li>{name}, {age}{~n}</li>
{:else}
<p>You have no friends!</p>
{/friends}
</ul>
PHP (remember, could be objects instead of associative arrays):
[
'friends' => [
[ 'name' => 'Moe', 'age' => 37 ],
[ 'name' => 'Larry', 'age' => 39 ],
[ 'name' => 'Curly', 'age' => 35 ]
]
]
This renders html as expected:
<ul>
<li>Moe, 37</li>
<li>Larry, 39</li>
<li>Curly, 35</li>
</ul>
If we change the friends array to be empty, the {:else}
block is triggered
[
'friends' => []
]
In the original Dust, it does not trigger the {:else}
block. LinkedIn's version fixed it, to keep # and ? consistent
Take special care if you are trying to pass a boolean parameter. param=true and param=false do not pass true/false as you might expect. They are treated as references to variables named "true" and "false". Unlike PHP, they are not reserved names. Note that they are not reserved in property names either so you can have a property named true or false. So you might think to pass 0 and 1 to your boolean-like parameter. That won't work either. Dust's boolean testing (i.e. {?xxx}
) is more of an existence test than a boolean test. Therefore, with param=1 and param=0 both value exists and so are considered true. Your best bet is to pass 1 and "", e.g. param=1 or param="". You could also leave off param="" if you are sure the name is not elsewhere in your data and accessible.
Dust PHP traverses and resolves values from several forms of data. Data can be in the form of an associative array, dynamic object (i.e. stdClass), class properties/methods, or even anonymous functions (i.e. closures). Here is a more advanced example:
use Dust\Evaluate\Chunk;
use Dust\Evaluate\Context;
use Dust\Evaluate\Bodies;
use Dust\Evaluate\Parameters;
class Info
{
public $extensions = ['PHP' => 'php', 'JavaScript' => 'js', 'Python' => 'py'];
public function getTextFile(Chunk $chunk, Context $context, Bodies $bodies = null, Parameters $params = null)
{
//get textFile context value
$textFile = $context->get('textFile');
if (empty($textFile)) {
$chunk->setError('Unable to find textFile in context');
} elseif (!is_file($textFile)) {
$chunk->setError('Unable to find ' . $textFile);
} else {
return $chunk->write(file_get_contents($textFile));
}
}
}
$context = [
'info' => function () {
return new Info();
},
'dateString' => function () { return date(DATE_RFC822); },
'arrayAccess' => new \ArrayObject(['foo' => 'bar']);
];
Now the following template:
{#info textFile="myfile.{info.extensions.JavaScript}"}
{#extensions}
Language {$idx} has extension .{.}{~n}
{/extensions}
{#getTextFile}Contents of {textFile}: {.}{~n}{/getTextFile}}
{/info}
It is currently {dateString}{~n}
Look, I can even use array access: {arrayAccess.foo}
As expected, this might output:
Language PHP has extension .php
Language JavaScript has extension .js
Language Python has extension .py
Contents of myfile.js: alert('Hello');
It is currently Fri, 21 Dec 2012 11:11:59 UTC
Look, I can even use array access: bar
Remember, logic in templates may not be what you want, but Dust PHP gives you the freedom to make that choice.
A Dust template named "xxx" is authored in a file named xxx.dust. You can have multiple .dust files and reference one Dust template as part of another one. This is the basis for "components" or reusable templates for tasks like a common header and footer on multiple pages.
Let's peek under the covers to see how the Dust template rendering knows about a template. As we said earlier, Dust templates are compiled to serializable PHP AST objects. Part of that compiled result is a call to $dust->register(name, template). The register call associates a template name with the function to run that template. So consider this example of how partials might be used:
{>header /}
... template for the body of the page...
{>footer /}
As long as the objects for the header.dust and footer.dust templates are loaded and registered prior to executing this template, it will run the header template, then its own body view and finally the footer template.
The partial reference syntax {>name /}
also supports paths so you can have a template at a path like "shared/header.dust" and reference it as {>"shared/header" /}
. This allows partials to be organized into library-like structures using folders. (see below for loading from file-system)
Like sections, partials accept parameters so you can build reusable components that are parameterizable easily. This gives you the same foundation for building libraries as other languages. By passing all the data into the partial using parameters, you isolate the partial from any dependence on the context when it is invoked. So you might have things like {>header mode="classic" /}
to control the header behavior.
Just like in sections, inline parameters will not override the current context if a property of the same name exists. For example, if the current context already has name = "Albert" adding name as a parameter will not override the value when used inside the partial foo.
{>foo name="will not override Albert"/}
Another caution: if you use parameters to pass a context like:
[
'homeAddress' => [
'street' => '1 Main St',
'city' => 'Anytown'
]
]
{>displayAddress address=homeAddress /}
then you will not be able to reference {address.street}
or {address.city}
in the body of the partial. These get treated as a path reference and the params are higher in the context stack at the point of reference so cannot be found. You need to code such things as:
{#address}
{street} {city}
{/address}
Note that you can also use dynamic partials, that conditionally select the partial to render based on the value in the data.
{>"flowViews/flowView{flowName}" /}
This sort of usage might suit a case where you have a multi-page flow and the controller could pass "page1", "page2",... in the data model to dynamically choose which partial to use to implement the view.
If compileFile
is used originally without an override name, it will have a directory and all referenced partials (that haven't already been loaded) will be assumed to have the ".dust" extension and attempt to load relative to the template's path. You can also add strings to the includedDirectories
array on the Dust object. If a referenced partial name is not found relative to the template's path (or there is no relative path info), it will attempt to find the template relative the paths in the includedDirectories
array, in order.
The autoloader can also be overridden either by setting the autoloaderOverride
to a callable, or simply extending the main Dust class and overriding loadTemplate
. Otherwise, Dust will look for previously loaded templates of the same name, then if relative file info for the template is present or at least one include directory is set, it will attempt to resolve the name as a path (appending ".dust" to the filename if needed).
{@select key="xxx"}
+ @eq
, @lt
, @lte
, @gt
, @gte
, @default
Select provides a key value that can be tested within its scope to output desired values. It mimics the switch/case statement. Here are some examples:
{@select key="{foo}"}
{@eq value="bar"}foobar{/eq}
{@eq value="baz"}foobaz{/eq}
{@default} - default Text{/default}
{/select}
{@select key=foo}
{@gte value=5}foobar{/gte}
{/select}
Each test condition is executed and if true, the body is output and all subsequent condtions are skipped. If no test condition has been met and a @default
is encountered, it will be executed and the select process terminates.
The @eq
(for example) can be used without a {@select}
.The most common pattern of usage would be for an HTML <select>/<option> list to mark the selected element with a "selected" attribute. The code for that looks like this where {#options}
is an array of options from the data model. Here the key is directly on the eq rather than on the select helper.
<select name="courses">
{#options}
<option value="{value}"{@eq key=value value=courseName} selected="true"{/eq} >{label}</option>
{/options}
</select>
Similarly, {@lt}
, {@gt}
, {@lte}
, {@gte}
can be used standalone and allow nesting. The following is a valid example
{@eq key="CS201 value=courseName}
{@eq key="CS101" value=prereq}
print it is CS201 course and has CS 101 as prereq
{/eq}
{/eq}
{@math}
- math helper
The math helper provides simple computational capabilities. Operations supported are: add, subtract, multiply, divide, mod, abs, floor, and ceil. The general syntax is:
{@math key="operand1" method="mathOpName" operand="operand2" /}
The helper computes a result using the key, method, and operand values. Some examples will clarify:
{@math key="16" method="add" operand="4"/} - Result will be 20
{@math key="16.5" method="floor"/} - Result will be 16
{@math key="16.5" method="ceil"/} - Result will be 17
{@math key="-8" method="abs"/} - Result will be 8
{@math key="{$idx}" method="mod" operand="2"/} - Return 0 or 1 according to $idx value
@math
with bodies
Sometimes you need to choose something to output based on the result of a math helper computation. For example, if the table row number is odd, you want to give it a gray background.
{@math key="{$idx}"" method="mod" operand="2"}
{@eq value=0}
show if $idx mod 2 == 0
{:else}
show if $idx mod 2 != 0
{/eq}
{/math}
The above evaluates the mod with the given key and operand i.e $idx % 2 and then checks if the output is 0, and prints the block inside the @eq
helper, if not the else block. Be careful to use numeric values for tests and not strings, e.g. {eq value="0"}
will never be true.
Another example
{@math key="13" method="add" operand="12"}
{@gt value=123}
13 + 12 > 123
{/gt}
{@default}
Math is fun
{/default}
{/math}
Using the nested @eq
, @lt
, etc. syntax allows you to output values like a select/case similar to the select helper.
{@if cond="condition"}
- if helper
There are a few cases where a simple true/false or exists/non-exists or single eq or lt or gt test won't suffice. For those, there is the if helper.
WARNING: this helper is dangerous and disabled by default in Dust PHP since it uses eval. To turn it on, run $dust->helpers['if'] = new \Dust\Helper\IfHelper();
.
Some examples
{@if cond="{x} < {y} && {b} == {c} && strlen('{e}') || strlen('{f}')"}
<div> x is less than y and b == c and either e or f exists in the output </div>
{/if}
{@if cond="({x} < {y}) || ({x} < 3)"} <div> x<y and x<3 </div> {/if}
{@if cond="{x} < {y} && {b} == {c} && strlen('{e}') || strlen('{f}') "}
<div> x is less than y and b == c and either e or f exists in the output </div>
{:else}
<div> x is >= y </div>
{/if}
Caveat #1: In the above example, if there is a possibility of undefined or false value for the {x}
or {y}
in the data, the correct syntax would be to check it exists and then check for {x} > {y}
. This is a known limitation since, {x}
returns nothing when the value of x is undefined or false and thus results in invalid PHP condition in the if helper.
{@if cond="!empty('{x}') && !empty('{y}') && {x} < {y} && {b} == {c} && strlen('{e}') > 0 || strlen('{f}') > 0 "}
<div> x is less than y and b == c and either e or f exists in the output </div>
{/if}
Caveat #2: The if helper internally uses PHP eval, for complex expression evaluation. Excessive usage of if may lead to sub-optimal performance with rendering, since eval is known to be slow.
{@sep}
- separator helper
When outputting lists of things, you often need to do something different for the last iteration. Consider the case
My friends are:
{#friends}
{name},
{/friends}
As written this will produce Hurley,Kate,Sawyer,Desmond, leading to the "dangling comma problem". This can be fixed by using the {@sep}
helper tag as follows:
My friends are:
{#friends}
{name}{@sep},{/sep}
{/friends}
The {@sep}
helper tag will output it's body content unless this is the final iteration of the containing loop.
{@size key="xxx"/}
- size helper
The size helper computes the size of the key parameter. The size computed depends on the type of the subject parameter as follows:
{@contextDump key="current|full" to="output|console"/}
- contextDump helper
The contextDump helper outputs the current context portion of the data model to the output stream using JSON. This can help with debugging if you suspect the context data is not as expected or you aren't sure what the current context is. If you want to change the defaults of key="current" and to="output", use the parameters. Remove this tag when done debugging.
An important need in developing a multi-page web application is to have common elements of the pages defined just once and shared by all pages (Don't Repeat Yourself). Dust provides this with the concept of blocks. Consider a common case where several pages share a header and footer but have different body content.
Blocks in the base template can contain default content and a child template can override that content. A block tag has the form {+name}default content{/name}
. In the following example, the base template has three blocks: pageHeader, bodyContent, and pageFooter. The pageHeader and pageFooter have default content that is shown if the child template does not override them.
<div class="page">
<h1>{+pageHeader}PayPal{/pageHeader}</h>
<div class="bodyContent">
{+bodyContent/}
</div>
<div class="footer">
{+pageFooter}
<hr>
<a href="/contactUs">Contact Us</a>
{/pageFooter}
</div>
</div>
Now that we have defined a base template with named blocks pageHeader, bodyContent, and pageFooter, let's look at how a child template can use it to supply body content and override the pageFooter. First, you insert the base template as a partial. Then you use one or more "inline partials" defining the values for the named blocks in the template.
Child template{! First, insert the base template as a partial !}
{>"shared/base_template"/}
{! Then populate the base template named blocks. Supply the desired bodyContent and pageFooter !}
{<bodyContent}
<p>These are your current settings:</p>
<ul>
<li>xxxx</li>
<li>yyy</li>
</ul>
{/bodyContent}
{<pageFooter}
<hr>
<a href="/contactUs">About Us</a> |
<a href="/contactUs">Contact Us</a>
{/pageFooter}
Note that inline partials like {<name}xxx{/name}
define "name" globally within the template. While this might be useful, remember the pains caused by global variables and use these with the knowledge that others can stomp on your chosen name inadvertently.
Dust helpers are PHP callable registered with the $dust->helpers
array with the name as the index. Thus the general form of a helper is (using an anonymous function):
$dust->helpers['myHelper'] = function ($chunk, $context, $bodies, $params) {
//code of the helper
}
All parameters definitions are optional and can be type hinted. As far as the parameters go:
$chunk
is the currently accumulating output of the template render process. You will most likely contribute additional output as part of your helper. This is an instance of Dust\Evaluate\Chunk
.
$context
is the current context stack (e.g that which changes when you do things like {#list}
). This is an instance of Dust\Evaluate\Context
.
$bodies
holds any body sections nested within the helper. For example, the {:else}
body. This is an instance of Dust\Evaluate\Bodies
.
$params
is an object that holds all the parameters used when calling the custom helper. This is an instance of Dust\Evaluate\Parameters
.
Here is a sample custom helper that implements a substring capability.
{@substr str="xxx" begin="x" end="y" len="z" /}
The annotated code to implement it is:
$dust->helpers['substr'] = function ($chunk, $ctx, $bodies, $params) {
//make sure str is present
if (!isset($params['str'])) return $chunk->setError('Parameter required: str');
//parse parameters
$str = $params['str'];
$begin = isset($params['begin']) ? $params['begin'] : 0;
$end = isset($params['end']) ? $params['end'] : null;
$len = isset($params['len']) ? $params['len'] : null;
//if len is set, use it instead of end
if ($len !== null) {
return $chunk->write(substr($str, $begin, $len));
} elseif ($end !== null) {
return $chunk->write(substr($str, $begin, $end - $begin));
} else {
return $chunk->write($str);
}
};
Note, this example may not do the level of runtime validation you might want to if distributing your helper. If you need to work with the body of the helper, then the following will get it for you.
$body = $bodies->block;
To evaluate the body, you call $chunk->render($body, $context);
.
$context
is the Dust context stack. Normally you will just use the Dust get
method when retrieving values from the context stack. If you need a deeper knowledge, take a look at the code for Dust\Evaluate\Context::get
Dust filters are simple callables that accept a single parameter of any type and can return any type. Here is an example of a filter to strip tags, applied to the Dust object, and ran on some input:
//create filter
$dust->filters['striptags'] = function ($value) {
//not a string, nothing to do
if (!is_string($value)) {
return $value;
}
//otherwise strip the tags
return strip_tags($value);
}
//create a context
$context = ['text' => '<p>My paragraph!</p>'];
//compile something using this filter
$dust->compile('Without tags: {text|striptags}', 'test');
//render and dump output
echo($dust->render('test', $context));
As expected, this will output simply My paragraph!
without the tags. You can also implement the Dust\Filter\Filter
interface which has an apply method to override.
Dust is written in Pratphall. To compile, get the code from GitHub and run npm install
which should install all dev dependencies. The compilation uses Jake, so simply running ./node_modules/.bin/jake build
will do the build. Ignore warnings about "unknown type of call" in the test code (it's the code that translates the spec from dustjs). Specifically, the build will clear the src/ and tests/ folders, then compile the code from pratphall/ into these two folders.