Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Handlebars Bundle Laravel Package

jays-de/handlebars-bundle

Symfony bundle that integrates Handlebars templating with service configuration and rendering support, making it easier to use logic-less Handlebars templates in Symfony applications. It fits projects that want Handlebars available alongside the framework's view and dependency injection conventions.

View on GitHub
Deep Wiki
Context7

Getting Started

Treat this as a Symfony bundle, not a Laravel package — there is no service provider, no facade, no config/handlebars.php. If you're coming from Laravel, the realistic "getting started" path is deciding whether you actually need the bundle or just the underlying PHP Handlebars engine it glues together.

If you're in a Symfony app (the intended target):

composer require jays-de/handlebars-bundle

Then register the bundle the Symfony-2/3 way:

// app/AppKernel.php
$bundles = [
    new Jays\HandlebarsBundle\HandlebarsBundle(),
];

Templates live next to your Twig views (Resources/views/**/*.handlebars), and Symfony's templating layer picks the engine by file extension:

public function indexAction()
{
    return $this->render('AppBundle:Default:index.handlebars', [
        'title' => 'Hello',
        'items' => ['a', 'b'],
    ]);
}

As of 1.2.0 there's a second entry point: render_hbs() inside Twig. If you already have Twig views and only want Handlebars components (widgets, shared client/server templates, third-party snippet libraries), you no longer need to switch the whole view engine per route. Register the TwigExtension (see Resources/config/services.yml / bundle DI config for the service tag) and:

{# resources/views/page.html.twig #}
<div id="search">{{ render_hbs('components/search.hbs') }}</div>
<div id="cart">{{ render_hbs('components/cart.hbs', {"items": items}) }}</div>

This is the fastest first use case: keep Twig for page layout, drop Handlebars in for the pieces you also render in the browser.

Where to look first:

  1. DependencyInjection/Configuration.php in the bundle — still the only real documentation for cache paths, engine options, and helper/partial registration.
  2. The bundle's Resources/config/services.yml — tells you the service ID (usually handlebars) and which class it wraps (Handlebars\Handlebars from the jays-de/handlebars / salesforce/handlebars lineage), plus how TwigExtension is wired.
  3. The new TwigExtension class itself (1.2.0) — it's small; read it before trusting it, because the README usage snippet is the whole spec.
  4. The upstream PHP Handlebars README, because ~90% of your day-to-day API surface (render, registerHelper, registerPartial) is defined there, not in the bundle.

First use case that actually justifies it: templates shared between server and browser. You render the same .handlebars file server-side for first paint/SEO and precompile it with the JS handlebars npm package for client-side re-render. Post-1.2.0 you can do this surgically inside an existing Twig app instead of adopting Handlebars as the view engine.

Laravel caveat up front: the bundle is glue for Symfony's EngineInterface and template-name resolver. None of that transfers. TwigExtension is Twig-specific and useless to a Blade app — don't reach for it. In Laravel you'd skip the bundle entirely and adapt the underlying engine:

// AppServiceProvider::boot()
View::addExtension('handlebars', 'handlebars'); // needs a custom engine resolver

or bind Handlebars\Handlebars as a singleton and call it from a Blade partial or a mail builder. Budget half a day for that adapter; the bundle saves you none of it.

Implementation Patterns

Controller-level rendering (Symfony). Use the extension-based engine selection for full pages; inject the handlebars service directly when you need to render a string or a template outside the HTTP response cycle (emails, PDFs, queue payloads).

$html = $this->get('handlebars')->render('emails/receipt.handlebars', [
    'order' => $order->toArray(),
    'currency' => 'EUR',
]);

Embedded rendering inside Twig (new in 1.2.0). This is the pattern to reach for by default now. Two rules that keep it sane:

  • Pass data explicitly as the second argument. render_hbs('x.hbs') gets an empty context — it does not inherit the Twig context. The usage example with {"foo": "bar"} is the honest signature.
  • Treat render_hbs() output as already-escaped HTML. In Twig, use {{ render_hbs(...) }} only if the extension returns a Twig_Markup/SafeString (check the class); otherwise you'll double-escape or need |raw. Verify against the extension source rather than assuming — this is exactly the kind of thing not covered by release notes.
{# explicit context, explicit safety #}
{{ render_hbs('components/user-card.hbs', {
    "name": user.name,
    "avatarUrl": asset('images/placeholder.png')
}) }}

Helpers as the integration point. Custom helpers are where all real logic lives — there is no Blade-style @foreach sugar, no view composers, no $loop variable. Port Laravel conveniences explicitly:

$engine->registerHelper('route', new class implements \Handlebars\Helper {
    public function execute($template, $context, $args, $source) {
        return route($args[0]); // or your Symfony router
    }
});

Register these in a compiler pass or a services.yml tag (check the bundle for a tag like handlebars.helper; if absent, decorate the engine service). The same helper registry serves both direct renders and render_hbs() calls — helpers registered once work in both paths, which is the main reason to prefer the Twig extension over ad-hoc engine calls.

Partials as the layout system. Handlebars has no inheritance. You emulate Blade's @extends/@section with a layout partial plus a block helper, or by nesting {{> layout}} and passing body as a SafeString. Pick one convention on day one and document it — mixed conventions in a 50-template codebase is where this gets painful.

{{! resources/views/layouts/base.handlebars }}
<html><body>{{{ body }}}</body></html>

Shared-template workflow (the good pattern). Keep one source of truth:

  • .handlebars files in resources/views/shared/.
  • Server renders them via the bundle — or via render_hbs() if you're gradually introducing them into a Twig app — for initial HTML.
  • npm run build precompiles the same files into public/js/templates.js for the browser.
  • Guard against drift by snapshot-testing the PHP render output against the JS render output for a fixture dataset. Post-1.2.0 the fixture test should exercise both the controller path and the Twig-extension path so you know the two entry points agree.

Mail and async rendering. The engine is cheap to instantiate per job; render inside the queue worker rather than storing HTML in the payload. This keeps template changes deployable without draining the queue.

Laravel-side pattern if you must have server-side Handlebars:

// A thin wrapper, no bundle involved
class HandlebarsRenderer
{
    public function __construct(private \Handlebars\Handlebars $engine) {}

    public function render(string $view, array $data): string
    {
        $path = resource_path("views/handlebars/{$view}.handlebars");
        return $this->engine->render(file_get_contents($path), $data);
    }
}

Register helpers for csrf (emit a meta tag, not a hidden field — the escaping story differs from Blade), asset, and old. Do not try to port @csrf field generation through a helper; keep forms in Blade and use Handlebars only for the shared display components. Note there is nothing in 1.2.0 that helps here — the Twig extension is dead weight for Laravel.

Gotchas and Tips

A new release does not mean an active project. 1.2.0 is the first release in years and adds one utility class; it does not modernize the bundle. It still targets Symfony 2/3-era APIs and PHP 7.x, and the underlying engine still predates PHP 8.1 dynamic-property tightening. Pin php and symfony/* constraints, vendor-patch where needed, and don't assume the version bump implies security maintenance. Read the 1.2.0 diff before upgrading — a one-class feature release is exactly the kind of change that ships without touching CI, so you're the test suite.

render_hbs() is a thin function, so all the ambiguities are yours to resolve. Confirm in the source: (a) whether the returned string is marked safe for Twig autoescaping, (b) what the template lookup root is (likely the same loader as the engine service — verify it resolves template.hbs relative to your Symfony view paths), and (c) whether the context array is passed as-is or marshalled (objects vs arrays, Twig_Markup values, null handling). None of that is in the release notes. Wrap it in a one-line Twig helper of your own if the semantics don't match your expectations, rather than sprinkling |raw around.

Two engines, two escaping rules, one page. With render_hbs() you now have Twig escaping and Handlebars escaping in the same document, and they are not identical (quotes/backticks differ). Data passed into render_hbs() has usually already been through a Twig expression — do not pre-escape it, and do not assume {{ }} in the .hbs file protects you the way Twig's autoescape would (it's string escaping, not contextual).

{{{ }}} is an XSS footgun and it's everywhere. Double-stash escapes, triple-stash does not. Layout partials almost always need triple-stash for the body, which means one careless {{{ userComment }}} in a nested partial and you've shipped an injection. This gets worse with Twig embedding, because reviewers see a Twig template and apply Twig-level trust to the embedded .hbs file. Audit every triple-stash in review, and keep .hbs files in a path that your Twig-focused reviewers are explicitly told to check.

Cache invalidation is the classic failure. These engines compile to PHP and cache to disk. Symptoms: template edits invisible in dev, stale markup after deploy, "works locally, broken in prod." render_hbs() runs through the same engine service, so it inherits the same cache — a stale cache surfaces as a Twig page with a frozen component, which is even harder to attribute. Make the cache directory writable, clear it in your deploy script (cache:clear may not cover a custom path configured in the bundle config), and disable caching in the dev environment explicitly.

Error messages are unhelpful. You get a Handlebars\Exception with little template context — no line numbers you can trust and often no template name. Wrap every render call in a try/catch that logs the template name and array_keys($data), and rethrows with that context. With render_hbs() in play an exception blows up the whole Twig page with a message that never mentions Twig, so log the Twig template name alongside the .hbs name — otherwise you'll be bisecting a page layout to find a broken component.

Spec drift from the JS implementation. The PHP port lags handlebars.js: block params ({{#each items as |item|}}), @root/@index availability, lookup, and whitespace control have all varied by version. If you're sharing templates with the browser, verify every helper and syntax feature in both runtimes with fixture tests before adopting it — this is the single biggest source of "renders fine on the server, blank on the client" bugs. 1.2.0 changes nothing about the engine version, so this risk is unchanged.

Debugging technique that works: render with a fixture array and var_dump the output, and keep a debug helper registered that dumps the context ({{debug}}) so you can see exactly what shape the data has inside a nested {{#each}}. Handlebars' context scoping (../, this) confuses everyone on first contact. For the Twig path specifically, temporarily render {{ render_hbs('debug.hbs', {"payload": someVar}) }} with a debug.hbs of {{debug}} to confirm what actually arrives after Twig has evaluated the context expression.

Extension points worth knowing: Handlebars\Helper for functions, Handlebars\Loader if you need partials from a database or S3 instead of the filesystem, the bundle's engine service decoration if you need per-tenant template roots, and now TwigExtension/render_hbs as a call-site convenience — note it is not itself an extension point, it's a wrapper, so customization (custom loader, per-request helpers) still happens on the engine service and is inherited automatically. There is no event system, no middleware, no "view composer" equivalent — decoration and helper registration are it.

The honest recommendation for a Laravel team: use Blade (or Livewire/Inertia) for app views and reach for Handlebars only if you have a hard requirement for identical client/server templates. In that case skip this bundle entirely and depend on the upstream PHP Handlebars engine directly — you get the same functionality, one less abandoned layer, no Symfony coupling, and full control over the Laravel view-engine adapter you'll be writing either way. 1.2.0 does not change that calculus; the new feature is Twig-only and if anything deepens the bundle's Symfony/Twig orientation.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment