Skip to content

Helper and filter cookbook

Justin Hileman edited this page Apr 30, 2026 · 2 revisions

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.

Enabling filters

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.

Basic value filters

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

Translating template strings

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.

Named date formats

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

Filtered sections

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-style foreach

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.

Loop state

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

Testing a list without iterating it

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

Restricting context lookup with only

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.

Further reading

Clone this wiki locally