-
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 }}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.