-
Notifications
You must be signed in to change notification settings - Fork 430
Helper and filter cookbook
Helpers and filters are a good fit for presentation-only transformations: formatting a value, reshaping data into a template-friendly form, or making section behavior explicit.
If the transformation is part of your domain model, prefer preparing a ViewModel before rendering. If the transformation is a reusable presentation concern, a helper or filter can keep the template small without adding a one-off field to every data object.
Filters are enabled with the {{% FILTERS }} pragma:
{{% FILTERS }}
{{ name | upcase }}Or globally:
<?php
$mustache = new \Mustache\Engine([
'pragmas' => [\Mustache\Engine::PRAGMA_FILTERS],
]);See FILTERS pragma for the syntax and caveats.
Register helpers as callables, then pipe values through them:
<?php
$mustache = new \Mustache\Engine([
'pragmas' => [\Mustache\Engine::PRAGMA_FILTERS],
'helpers' => [
'upcase' => fn ($value) => strtoupper((string) $value),
'join' => fn ($value) => implode(', ', $value),
],
]);{{ name | upcase }}
{{ tags | join }}Use a section helper for template strings that should be translated. The helper receives the raw section body, returns a translated template string, and Mustache renders the result in the current context:
<?php
$translations = [
'Hello.' => 'Hola.',
'My name is {{ name }}.' => 'Me llamo {{ name }}.',
];
$mustache = new \Mustache\Engine([
'helpers' => [
'i18n' => fn ($text) => $translations[$text] ?? $text,
],
]);{{# i18n }}Hello.{{/ i18n }}
{{# i18n }}My name is {{ name }}.{{/ i18n }}If your application already uses gettext or WordPress translations, prefer using that translation system directly:
<?php
$mustache = new \Mustache\Engine([
'helpers' => [
'i18n' => fn ($text) => gettext($text),
],
]);For existing gettext catalogs with %s placeholders, map Mustache tags to placeholders before lookup, then put the tags back so Mustache can render them:
<?php
$mustache = new \Mustache\Engine([
'helpers' => [
'i18n' => function ($text) {
$tags = [];
$message = preg_replace_callback('/{{\s*[^}]+\s*}}/', function ($matches) use (&$tags) {
$tags[] = $matches[0];
return '%s';
}, $text);
return vsprintf(gettext($message), $tags);
},
],
]);{{# i18n }}Account balance: {{ balance }}{{/ i18n }}Note
xgettext and Poedit keyword extraction look for function calls, not Mustache section tags. To build .pot files from templates, add a Mustache-aware extraction step that walks parsed templates and extracts the i18n section bodies.
Filters do not take arguments. To support multiple date formats, group named formatters under a helper and choose the format by name:
<?php
function format_date($value, $pattern) {
$date = $value instanceof DateTimeInterface ? $value : new DateTimeImmutable($value);
return $date->format($pattern);
}
$mustache = new \Mustache\Engine([
'pragmas' => [\Mustache\Engine::PRAGMA_FILTERS],
'helpers' => [
'date' => [
'atom' => fn ($value) => format_date($value, DateTimeInterface::ATOM),
'short' => fn ($value) => format_date($value, 'M j, Y'),
'year' => fn ($value) => format_date($value, 'Y'),
],
],
]);{{ published_at | date.short }}
{{ published_at | date.atom }}
{{ published_at | date.year }}Filters can also be used in sections. The filter receives the value, returns a new value, and the section renders against the filtered result.
<?php
$mustache = new \Mustache\Engine([
'pragmas' => [\Mustache\Engine::PRAGMA_FILTERS],
'helpers' => [
'foreach' => function ($value) {
if ($value instanceof Traversable) {
$value = iterator_to_array($value);
} elseif (is_object($value)) {
$value = get_object_vars($value);
}
if (!is_array($value)) {
throw new UnexpectedValueException('foreach expects an array or Traversable value');
}
$last = count($value) - 1;
$items = [];
$index = 0;
foreach ($value as $key => $item) {
$number = $index + 1;
$items[] = [
'key' => $key,
'value' => $item,
'loop' => [
'index' => $index,
'index0' => $index,
'index1' => $number,
'first' => $index === 0,
'last' => $index === $last,
'odd' => $number % 2 === 1,
'even' => $number % 2 === 0,
],
];
$index++;
}
return $items;
},
'upcase' => fn ($value) => strtoupper((string) $value),
],
]);{{# states | foreach }}
{{ key | upcase }}: {{ value }}
{{/ states }}Note
You may omit filters from the closing section tag.
PHP uses one array type for both lists and hashes. Mustache.php iterates consecutive numeric arrays and treats associative arrays as section context. Use a foreach filter when you want PHP-style iteration with key, value, and loop state:
<?php
$data = [
'settings' => [
'host' => 'localhost',
'port' => 6379,
],
];{{# settings | foreach }}
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
{{/ settings }}For more complicated needs, or behavior that should be reused across templates, consider using a ViewModel or presenter class instead of an inline helper. See EnumerationPresenter and Using a real ViewModel with Mustache.
If a template needs index, first, last, odd, or even, add that data in a helper or presenter rather than calculating it in the template. The foreach helper above exposes this as loop data. The loop.index and loop.index0 values are zero-based, like PHP array indexes, while loop.index1 is one-based.
{{# users | foreach }}
<li class="{{# loop.odd }}odd{{/ loop.odd }}{{# loop.even }}even{{/ loop.even }}">
{{ loop.index1 }}. {{ value.name }}
{{# loop.first }}first{{/ loop.first }}
{{# loop.last }}last{{/ loop.last }}
</li>
{{/ users }}Mustache sections really like to iterate, but that's not always what you want. To use a list to conditionally render a section, you can use an empty? helper:
<?php
$mustache->addHelper('empty?', function ($value) {
if ($value instanceof Countable) {
return count($value) === 0;
}
if ($value instanceof Traversable) {
foreach ($value as $_) {
return false;
}
return true;
}
return empty($value);
});{{# jobs | empty? }}
No jobs found.
{{/ jobs }}
{{^ jobs | empty? }}
<ul>
{{# jobs }}
<li>{{ title }}</li>
{{/ jobs }}
</ul>
{{/ jobs }}Mustache normally resolves variables by walking up the context stack. This is usually exactly what you want, but sometimes it's convenient to restrict the scope of a section to a single value.
The only filter wraps the current value in an object that masks parent context lookup:
<?php
class OnlyValue
{
private $value;
public static function wrap($value)
{
if ($value instanceof Traversable) {
$value = iterator_to_array($value);
}
if (self::isList($value)) {
return array_map([self::class, 'wrap'], $value);
}
return new self($value);
}
public function __construct($value)
{
$this->value = $value;
}
public function __isset($name)
{
return true;
}
public function __get($name)
{
if (is_array($this->value) && array_key_exists($name, $this->value)) {
return $this->value[$name];
}
if (is_object($this->value)) {
if (method_exists($this->value, $name)) {
return $this->value->$name();
}
if (isset($this->value->$name)) {
return $this->value->$name;
}
}
return '';
}
private static function isList($value)
{
if (!is_array($value)) {
return false;
}
$i = 0;
foreach ($value as $key => $_) {
if ($key !== $i++) {
return false;
}
}
return true;
}
}
$mustache->addHelper('only', fn ($value) => OnlyValue::wrap($value));{{ title }}
{{# items | only }}
{{ title }}
{{/ items }}Inside items, {{ title }} now renders only an item title. If the item does not have a title, it renders an empty string instead of falling back to the parent title.
This is similar to ANCHORED-DOT pragma, which anchors an individual name lookup to the top context frame.
- FILTERS pragma
- Mustache tags
- Variable Resolution
- ANCHORED-DOT pragma
- i18n with parameters discussion
- Mustache spec i18n discussion
- Mustache.php i18n and gettext discussion
- Filtered section and
onlyhelper discussion - Simple FILTERS example fixture
- EnumerationPresenter
- Using a real ViewModel with Mustache