mthaml/mthaml-bundle
A Symfony bundle that integrates MtHaml, the PHP implementation of the HAML template language. Write concise, indentation-based templates that compile to clean HTML, with seamless integration into Symfony's templating system, view helpers, and caching.
Reality check first. This is a Symfony2 bundle, not a Laravel package. There is no service provider, no Blade compiler hook, no config/view.php integration. If you're in a Laravel codebase, the bundle is not the thing to install — the only sane path is the underlying compiler (mthaml/mthaml) wrapped in your own view adapter. Treat the rest of this section as "day-to-day in an inherited Symfony 2.x app," which is the only context where the bundle is actually leverageable.
Install under the Packagist name, not the repo name. The GitHub repo is arnaud-lb/MtHamlBundle, but Composer resolves it as mthaml/mthaml-bundle:
composer require mthaml/mthaml-bundle
Do this on a branch, not on main. Read composer.json before you commit — the Symfony constraints will tell you instantly whether your app version is in scope.
Register the bundle in app/AppKernel.php:
$bundles = array(
// ...
new MtHamlBundle\MtHamlBundle(),
);
Where to look first — three files, in this order:
DependencyInjection/Configuration.php — the authoritative mt_haml.* config tree. The README lags; the config node doesn't. Everything from escaping mode to which engine (PHP vs Twig) you compile to is decided here.Resources/config/*.xml — service definitions. You'll want the mt_haml service id and the templating/Twig wiring around it, especially if you need to override or decorate the engine.vendor/mthaml/mthaml/README.md — the bundle is thin glue. The language itself (indentation rules, attribute syntax, filters, escaping semantics) lives in the core library, and that README is the real reference you'll be reading every day.First working template. Use Symfony's logical-name convention and put .haml in the filename so the templating layer selects the engine by extension:
src/Acme/DemoBundle/Resources/views/Default/index.html.haml
!!! 5
%html
%head
%title= $view['slots']->get('title', 'Demo')
%body
%h1 Hello, #{$name}
%ul
- foreach ($items as $item):
%li= $item
- endforeach
Render it exactly like any other template from the controller:
return $this->render('AcmeDemoBundle:Default:index.html.haml', array(
'name' => 'World',
'items' => array('a', 'b', 'c'),
));
If that renders, setup is finished. There's no asset pipeline, no console command, no build step — which is both the appeal and the operational trap (more on that in Gotchas).
Decide PHP-target vs Twig-target on day one, and never mix. MtHaml compiles HAML into either PHP code or Twig source (new MtHaml\Environment('php') vs 'twig'). The bundle's config chooses which. This determines what goes on = and - lines:
= / - lines are PHP. You get $view, $view['slots'], $view['router'], $view['assets'], and the extracted template variables directly. Correct choice for a legacy app already on PhpEngine-style templates.= / - lines carry Twig expressions and tags, so path(), asset(), trans, and every Twig extension come for free. Requires TwigBundle present.Mixing the two in one template is the single most common source of "why is this printing $foo literally" bugs. Write it down as a team rule.
Layouts. PHP mode:
- $view->extend('AcmeDemoBundle::layout.html.haml')
- $view['slots']->set('title', 'Detail page')
%p= $record->getTitle()
Twig mode:
- extends 'AcmeDemoBundle::layout.html.twig'
- block content
%p= record.title
Partials. Rendering a sub-template is a normal call; the only HAML-specific thing is indentation discipline:
%aside
= $view->render('AcmeDemoBundle:Widget:sidebar.html.haml', array('items' => $items))
Attribute-heavy markup is where HAML earns its keep. Use the array/hash syntax when you have dynamic classes or multiple attributes, and prefer = for values so escaping runs through the configured escaper:
%a{href: $view['router']->generate('user_show', array('id' => $user->getId())), class: 'user-link'}
= $user->getName()
Static markup scales poorly into helpers. Resist the urge to wrap every loop in a custom filter. HAML filters are for content transformation (markdown, textile, etc.), not for control flow. Control flow belongs in - foreach, - if, or in Twig blocks.
Integration tip — isolate behind one boundary. Because this is an EOL-framework dependency with a single maintainer, do not let .haml filenames or MtHaml\* classes leak into controllers, services, or view models. Route all rendering through your existing render() helper and keep the bundle a swappable detail of the templating layer. That single discipline is what turns a migration from "rewrite the app" into "rewrite the templates."
Migration pattern. If the medium-term plan is Twig, you can migrate incrementally: the Templating component resolves engine by extension, so .html.haml and .html.twig coexist. Convert leaf partials first, layouts last, and run both engines in parallel until the HAML count reaches zero.
The package name will bite you. composer require arnaud-lb/MtHamlBundle fails; only mthaml/mthaml-bundle resolves. Same for docs and Stack Overflow searches — search the Packagist name.
Whitespace is syntax. A stray space or a tab-indented line is a parse error at best and a silently wrong structure at worst. Enforce spaces (never tabs), and make your editor show invisibles. Review diffs on .haml files more carefully than you're used to — a one-space shift is a real change.
PHP-mode templates compile to PHP you'll have to read eventually. When something renders wrong, don't debug the HAML in your head — find the compiled file in the Symfony cache and read it. The generated PHP is ugly but unambiguous, and it instantly answers "did my = become an echo or not." Clear the cache (rm -rf app/cache/* or the console equivalent) after config changes, because stale compiled templates are the number one "my change did nothing" cause.
Escaping is a config decision, not a per-template one. The escaper is set on the HAML environment and applied through the compiled output. If you see double-escaped entities, someone is escaping manually on top of the auto-escaper. If you see raw HTML, either the escaper is off or the value was marked safe. Audit this in Configuration.php before shipping anything user-facing, and write one test per template that covers a value containing < and ".
Caching is compile-time, not free. The Templating layer compiles HAML to PHP/Twig and caches the result. In dev that's convenient; in prod it means you must warm the cache during deploy, and you must invalidate it when templates change. If you're considering runtime compilation in a high-traffic path, don't — precompile at build time or accept the CPU cost knowingly.
Filters and escapers are the extension points. The core library defines filter and escaper interfaces; the bundle is how you wire custom implementations into the mt_haml service. If you need a house-specific filter (say, an internal asset URL rewriter), that's the correct seam — not string post-processing on rendered output. Check Resources/config/*.xml for the exact service wiring before writing anything.
Symfony version assumptions are hard boundaries. The bundle targets the EngineInterface-era templating stack that was removed in later Symfony majors. If you're on Symfony 3+, expect to either pin to an old component set or fork the templating wiring. Verify on a branch before you promise anyone a timeline.
No upstream safety net. 38 stars, zero dependents, single maintainer, EOL framework target. Treat it as vendored code: pin the exact version in composer.json, commit composer.lock, and assume no CVE will be patched for you. Flag it in dependency/SBOM reviews so the risk is visible rather than quietly accumulating.
Onboarding cost is real and easy to underestimate. Developers who know Blade or Twig will read HAML markup fine and then write it wrong for a week. Budget for that, keep a short internal style page (spaces, attribute syntax, PHP-vs-Twig rule), and put a senior reviewer on .haml PRs until the team flattens the curve.
Debugging checklist, in order: compiled cache present and fresh → escaper mode matches your expectation → PHP-vs-Twig mode matches the =/- lines → indentation is spaces and consistent → the variable is actually extracted into scope. That sequence resolves the overwhelming majority of "it rendered weird" reports, and it's faster than reading the HAML parser's error messages, which are terse.
How can I help you explore Laravel packages today?