protonemedia/laravel-blade-on-demand
protonemedia/laravel-blade-on-demand lets you compile and render Blade templates on the fly—including strings and dynamically generated views—without creating view files. Ideal for user-defined templates, CMS content, and email builders, with caching for performance.
What it does: gives you a Blade compiler you can feed a template string instead of a view file. The template is compiled and evaluated in memory (compiled output is keyed/hashed rather than written as a normal view), so you can render Blade you pulled out of a database column, a CMS field, a tenant theme table, or an API payload.
Minimal steps
composer require protonemedia/laravel-blade-on-demand — check composer.json on the repo for the PHP/Laravel constraint before you commit; this is a small, single-maintainer package (62 stars, 0 dependents), so pin it and plan to be able to vendor it.use Protonemedia\BladeOnDemand\Facades\BladeOnDemand;
$html = BladeOnDemand::render(
'Hello, {{ $name }}! You have {{ $count }} new messages.',
['name' => 'Pascal', 'count' => 3]
);
Where to look first
src/ — the service provider (what it binds and whether it swaps the global Blade compiler) and the compiler class that overrides the compiled-path/expiry logic. That tells you whether custom Blade::directive() registrations apply.Blade::render($string, $data) natively. If you're on 9 or later and you only need "string in, HTML out," you may not need this package at all. Reach for it when you need its specific extras or you're pinned to Laravel 8.First real use case: a DB-backed email template. Store the subject/body in a templates table, let an admin edit the Blade source, and render with BladeOnDemand::render($template->body, $payload) before handing the HTML to Mail::html() or a Mailable.
DB template + file view fallback
$html = $template
? BladeOnDemand::render($template->body, $data)
: view("emails.{$key}", $data)->render();
Keep the file view as the source of truth for the default and seed the DB from it. That way a broken/empty DB row never takes down a critical flow.
Wrap it in a small service, not raw calls everywhere
Put one class between your app and the package: TemplateRenderer::render(string $key, array $data). It gives you a single place to add caching, logging, sanitization, and a fallback — essential given the package's size and bus factor.
Cache rendered output, not just compilation
Compilation caching only saves parsing. If a template is rendered for every request (e.g. a tenant banner), cache the result:
$key = 'tpl:'.md5($template->body).':'.md5(serialize($data));
$html = Cache::remember($key, now()->addHour(), fn () => BladeOnDemand::render($template->body, $data));
Invalidate on the template's updated_at, and be careful that $data contains no per-user secrets — otherwise you'll leak one user's rendering to another.
Multi-tenant / CMS overrides
Resolve the template by scope (tenant_id, locale), falling back up the chain: tenant → global → file view. This is the pattern the package is best suited for; it keeps theming data out of the filesystem and out of deploys.
Email, PDF, and notification bodies
Mail::html($html, $callback) or a Mailable that sets $this->html.Admin preview endpoint
Expose a "preview" route that renders the draft template inside a try/catch and returns the exception message to the editor UI. This is the single highest-value integration: it turns a opaque 500 in production into a red box next to the textarea.
try {
return response(BladeOnDemand::render($request->body, $sampleData));
} catch (\Throwable $e) {
return response()->json(['error' => $e->getMessage()], 422);
}
Testing
Assert on the rendered string directly — no HTTP, no view file:
$this->assertStringContainsString('Hello, Pascal!', BladeOnDemand::render($src, ['name' => 'Pascal']));
Snapshot-test the default templates so an accidental edit is visible in the diff.
Arbitrary code execution — treat stored templates as PHP, not data. {{ }}, {!! !!}, @php, @if, and every registered directive are available. If anyone other than a trusted developer can edit a template, you have a remote-code-execution vector. Mitigate with roles/permissions on the editor, an audit log of template versions, and — if you must let non-devs edit — restrict to a whitelisted subset of directives rather than accepting arbitrary Blade. Never render a template string that arrived in a request body.
$this and view context differ from a normal view. Code that relies on the view's internal context ($this->, @include of a view that expects shared data, component class resolution) can behave differently than under view(). Pass everything you need explicitly in the data array; don't assume view()->share() or the current route's variables are present.
Custom directives: verify they work. If the package swaps in its own compiler instance, directives registered on Laravel's default compiler may not apply. Test one directive (Blade::directive('money', ...)) before building features on top. If it fails, either register on the on-demand compiler or re-register in a service provider after the package boots.
Error messages point at compiled files, not your template. Compile exceptions reference a hashed compiled path and a line number in generated PHP. Map it back by dumping the compiled source, or by isolating the failing snippet in php artisan tinker and bisecting. A @php block or malformed directive is usually the culprit.
Laravel 9+ overlap. Blade::render() does the same core job natively and is maintained by the framework. If you're on 9+, prefer it and use this package only for a specific feature you can't replicate. Document why it's a dependency, or a future upgrade will silently leave dead weight.
No cross-request compile cache guarantee. If compilation happens per call and the compiled output is in-memory only, you pay the parse cost on every render. Benchmark before putting it on a hot path; add the render cache described above.
Escaping surprises. {{ $x }} escapes (good for user data), {!! $x !!} does not. Because admins control the template, it's easy for them to introduce an XSS by using {!! !!} around a user-supplied field. Lint or review templates that contain {!!.
@extends / @section from a string is awkward. Layout inheritance expects a view file to extend. For stored templates, prefer self-contained markup or @include of normal file-based partials for shared header/footer chrome.
Keep the package replaceable. It's ~one class of logic over BladeCompiler. Write your TemplateRenderer interface so that if the package goes unmaintained or you move to Blade::render(), you change one file. Check its release date and test suite before relying on it for a revenue-path feature, and pin the version in composer.json.
How can I help you explore Laravel packages today?