Development happens on gitlab.castris.com. The GitHub repository is a read-only distribution mirror: issues and pull requests opened there are not seen.
A framework-agnostic, fully testable, SOLID-compliant mustache template resolver for PHP applications with first-class Laravel integration.
- Simple field resolution:
{{User.name}} - Relation navigation:
{{User.department.manager.name}} - Dynamic fields:
{{Device.$manufacturer.field_parameter}} - Collection access:
{{User.posts.0.title}},{{User.addresses.*.city}} - Built-in functions:
{{now()}},{{format(User.date, 'Y-m-d')}} - Null coalescing:
{{User.nickname ?? 'Anonymous'}} - Framework-agnostic core with optional Laravel integration
- 100% testable without database
| Package version | PHP | Laravel | Status |
|---|---|---|---|
| 2.x | 8.2, 8.3, 8.4 | 12.x, 13.x | Active development |
| 1.x | 8.2, 8.3, 8.4 | 10.x, 11.x, 12.x | Security fixes only |
- PHP 8.2+
- Laravel 12.x or 13.x (optional)
composer require aichadigital/laravel-mustache-resolverThe package auto-discovers the service provider. Optionally publish the config:
php artisan vendor:publish --tag="mustache-resolver-config"use AichaDigital\MustacheResolver\Core\MustacheResolver;
use AichaDigital\MustacheResolver\Core\Parser\MustacheParser;
use AichaDigital\MustacheResolver\Core\Pipeline\PipelineBuilder;
use AichaDigital\MustacheResolver\Cache\NullCache;
$resolver = new MustacheResolver(
new MustacheParser(),
PipelineBuilder::create()->build(),
new NullCache()
);use AichaDigital\MustacheResolver\Laravel\Facades\Mustache;
$template = "Hello, {{User.name}}! Your email is {{User.email}}.";
$user = User::find(1);
$result = Mustache::translate($template, $user);
if ($result->isSuccess()) {
echo $result->getTranslated();
// "Hello, John! Your email is john@example.com."
}$template = "Manager: {{User.department.manager.name}}";
$result = Mustache::translate($template, $user);// Access by index
$template = "First post: {{User.posts.0.title}}";
// Access first/last
$template = "Latest: {{User.posts.last.title}}";
// Wildcard (returns array)
$template = "All cities: {{User.addresses.*.city}}";$template = "Report for {{$period}}: {{User.name}}";
$result = Mustache::translate($template, $user, ['period' => '2024-Q1']);$templates = [
"Name: {{User.name}}",
"Email: {{User.email}}",
"Department: {{User.department.name}}",
];
$results = Mustache::translateBatch($templates, $user);// Missing fields return empty string instead of failing
$result = Mustache::translate($template, $user, [], strict: false);// config/mustache-resolver.php
return [
'strict' => true, // Throw on unresolvable mustaches
'keep_unresolved' => false, // Keep mustaches if not resolved (non-strict)
'cache' => [
'enabled' => false,
'ttl' => 3600,
],
'security' => [
'mode' => 'report', // 'off' | 'report' | 'enforce'
'max_depth' => 10,
'blacklisted_attributes' => ['password', 'remember_token'],
],
];The service provider builds a SecurityValidator from config('mustache-resolver.security')
and applies it to every resolution path, both for Eloquent models and array data.
Every segment of a dot-notation path is checked against blacklisted_attributes, so a
blacklisted attribute is also blocked behind a relation ({{User.relationship.password}}),
and paths deeper than max_depth are rejected.
The security.mode setting controls what happens on a violation:
report(default): the violation is logged viaLog::warning()and resolution proceeds. Non-breaking: use it to discover what enforcement would block before upgrading. This will becomeenforcein v3.0.0.enforce: violations block the path (resolves to empty) and disallowed models throwModelNotAllowedException. Blocked attempts are also logged, as an audit trail.off: no checks are applied.
Blacklist matching is case-insensitive and covers every resolution path, including
collection tokens ({{User.posts.*.author.password}}). Serializing a whole relation
({{User.department}}) filters blacklisted attributes out of the serialized output
in enforce mode, preserving escapeWhenCastingToString(). Resolved arrays and
collections containing blacklisted attributes are reported in report mode (their
filtering is deferred to v3.0.0). Repeated violations of the same path are logged
once per request/job cycle. An invalid security.mode value fails closed
(enforce) with a warning.
use AichaDigital\MustacheResolver\Contracts\ResolverInterface;
class CustomResolver implements ResolverInterface
{
public function supports(TokenInterface $token, ContextInterface $context): bool
{
return $token->getPrefix() === 'Custom';
}
public function resolve(TokenInterface $token, ContextInterface $context): mixed
{
// Your resolution logic
}
public function priority(): int
{
return 150; // Higher than built-in resolvers
}
public function name(): string
{
return 'custom';
}
}Register in config:
'resolvers' => [
\App\Resolvers\CustomResolver::class,
],composer testPlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The AGPL-3.0-or-later License. Please see License File for more information.