hoa/zformat
Hoa\Zformat is a PHP library for defining and compiling format strings. It provides a grammar and compiler that transform custom format expressions into PHP code, making it easy to build extensible, reusable string formatting rules.
hoa/zformat is a small, standalone string-formatting library: you construct a Hoa\Zformat\Formatter around a template string, then call format() with a keyed array of values. It is printf-inspired but supports named placeholders, which makes it a better fit than sprintf() for user-facing templates where argument order is fragile.
composer require hoa/zformat
Composer will also pull hoa/consistency and hoa/exception — the only real dependency surface.
The first place to look is Hoa\Zformat\Formatter (src/Formatter.php in the repo). Everything else in the package is support code: grammar, exception, and the Hoa\Consistency bootstrap.
Minimal first use — a notification/order message with named fields:
use Hoa\Zformat\Formatter;
$formatter = new Formatter(
'Hi %(name)s, order #%(id)d shipped on %(date)s.'
);
echo $formatter->format([
'name' => $user->name,
'id' => $order->id,
'date' => $order->shipped_at->toDateString(),
]);
The first genuinely useful use case in a Laravel app is database- or config-driven message templates: a message_templates.body column full of %(...)s placeholders, rendered against a model's attributes at send time. That is safer and more readable than ad-hoc str_replace([':name'], ...) and avoids the positional-counting bugs of sprintf().
Verify the exact specifier/placeholder grammar against the repo's Documentation/ folder before committing — the project is archived, so this is the last word on it.
Wrap it in a small service. Don't scatter new Formatter(...) through controllers. Bind a renderer in a service provider so you can swap it later (very likely, given the archive status):
// app/Services/TemplateRenderer.php
namespace App\Services;
use Hoa\Zformat\Formatter;
class TemplateRenderer
{
public function render(string $template, array $data): string
{
return (new Formatter($template))->format($data);
}
}
// AppServiceProvider::register()
$this->app->singleton(TemplateRenderer::class);
Render Eloquent models via toArray() when the template keys map to attributes, or pass an explicit allow-list if templates are admin-editable (see Gotchas).
$renderer->render($template->body, $order->only(['id', 'total', 'shipped_at']));
Cache formatter instances for hot, repeated formats. The format string is parsed per call, so a loop over 10k rows re-parses 10k times. For constant formats, memoize:
$formatter = new Formatter($auditFormat);
foreach ($events as $event) {
$lines[] = $formatter->format($event->toArray());
}
Compose with localization. Keep translation out of the formatter: resolve the locale-specific template with trans()/Lang::get(), then hand it to Zformat. This keeps pluralization and locale switching in Laravel's translator, where they belong.
Blade directive (optional). If templates appear in views, register a directive rather than calling PHP in Blade:
Blade::directive('zformat', function ($expression) {
return "<?php echo app(\App\Services\TemplateRenderer::class)->render({$expression}); ?>";
});
Safe-render pattern for user-editable templates. Wrap calls and fall back to a sanitized default so a malformed placeholder in an admin template can't 500 a checkout flow:
try {
return $renderer->render($template, $data);
} catch (\Throwable $e) {
report($e);
return $fallback;
}
Testing. Assert on exact output strings; for malformed templates assert on Hoa\Zformat\Exception (or catch \Throwable, since the exception chain also includes Hoa\Exception).
It is archived and effectively unmaintained. Last release was 2017-01-10, the repository is archived, and there are zero dependents on Packagist. Treat it as a legacy dependency: pin a version, run it through composer audit, and have an exit plan (sprintf, strtr, Str::replaceArray, or Laravel's own :placeholder translator already do most of this).
PHP 8 compatibility is not guaranteed. The 2017-era hoa/consistency bootstrap is the likeliest failure point on modern PHP (deprecations, dynamic properties, E_STRICT-era patterns). Run your CI matrix before adopting; if it fails on PHP 8.2+, don't fight it — the library is trivially replaceable.
No escaping happens for you. Zformat is not an escaper. In Blade, still use {{ }}/e() for user-provided strings; if a rendered template is emitted unescaped, you have introduced XSS. Never use it to build SQL — use query bindings.
Never pass an untrusted format string. The format is effectively code-shaped; only the argument array should contain user data. For admin-editable templates, validate the placeholder set against an allow-list (['name', 'id', 'total']) before storing.
No Laravel integration ships with it. No service provider, no facade, no config, no auto-discovery. Everything above is your wiring.
Be explicit about missing variables. Formatting an array that is missing a key may leave the placeholder intact or throw, depending on the specifier. Decide which behavior you want and enforce it in your wrapper (validate array_diff_key first) so a missing %(name)s never reaches a customer's inbox.
Don't mix positional and named specifiers in one template — it makes debugging output unreadable and the behavior is easy to get wrong.
Type specifiers coerce. %(id)d on "abc" will not do what you want. Normalize values (casts, Carbon::toDateString()) before passing them in.
Exceptions come from the Hoa\ namespace. Catch Hoa\Zformat\Exception for format errors, but be aware the chain may include Hoa\Exception, so \Throwable is the pragmatic catch in boundary code.
No extension points, and no fork-in-anger. The grammar is fixed; there is no plugin mechanism for new conversions. If you need custom tokens, wrap the library and pre-process, or skip it — extending an archived package is a maintenance tax with no upstream.
Low ecosystem momentum. The opportunity score (33.23) and zero dependents are a signal: you will be the only person debugging this in your org. For a greenfield Laravel app, sprintf() for internal logs and __('...', ['name' => $name]) for user-facing strings covers 95% of the cases with zero dependency risk. Use Zformat when its named-placeholder grammar genuinely earns its keep — typically a template-rendering layer where templates live in the database and are edited by non-developers.
How can I help you explore Laravel packages today?