mercurysolutions/mathml-bundle
The MathML Bundle for Symfony makes it easy to add MathML support to PHP applications. It provides configurable integration and Twig helpers for rendering mathematical formulas in templates, helping you keep math markup clean and consistent.
First, know what you're actually installing. mercurysolutions/mathml-bundle is a Symfony bundle — it ships a Bundle class, a DependencyInjection extension, and (typically) a Twig extension that wraps the real workhorse: the mercurysolutions/phpmathml library. Laravel has no bundle kernel and no config/bundles.php, so installing the bundle itself buys you almost nothing you can boot. Check composer.json in the repo, then install the underlying library directly:
composer require mercurysolutions/phpmathml
Pull the bundle in only as a reference implementation (read src/Resources/config/services.yaml and the Twig extension to see how they wire the service and configure defaults). Don't add it to require in a Laravel app.
Note the 4.0.0 split. The bundle's 4.0.0 release (Symfony 7 support, PHP 8.2 floor, Symfony 2/3/4 dropped) tells you two useful things: the bundle is still alive, and it now tracks a Symfony generation Laravel doesn't use. Read it as a reference, not as a dependency — the wiring patterns in 4.x are current Symfony style, so any older blog snippets you find for 2.x/3.x will not match services.yaml or Configuration.php.
Where to look first, in this order:
vendor/mercurysolutions/phpmathml/src/ — the node classes. This is your real API surface, and 4.0.0 says nothing about it; the library has its own release cadence, so check its tags separately.mathml() function or a filter that accepts a built node and returns an XML string).DependencyInjection/Configuration.php — any default attributes/options worth replicating in your own config.Minimal wiring in Laravel. Bind the builder as a singleton and expose a Blade directive:
// app/Providers/MathMlServiceProvider.php
public function register(): void
{
$this->app->singleton(\PHPMathML\MathML::class, fn () => new \PHPMathML\MathML());
}
public function boot(): void
{
Blade::directive('mathml', fn ($expr) => "<?php echo app(\\PHPMathML\\MathML::class)->render($expr); ?>");
}
First use case: a product or education app that needs to display formulas server-side — e.g. quiz questions like x² + 3x − 4 = 0 — without shipping KaTeX/MathJax JS. Build the node tree in PHP, emit MathML, drop it into the Blade view. Modern Chromium (109+), Firefox, and Safari all render MathML Core natively, so for straightforward arithmetic you get zero-JS rendering.
Treat the library as a builder, not a string-concatenator. The idiomatic flow is: create a root <math> node, append children (mi identifier, mn number, mo operator, mfrac, msup, msqrt, mtable), then serialize. Wrap the whole thing in a small application-level factory so call sites read cleanly:
final class Formula
{
public function __construct(private \PHPMathML\MathML $mathml) {}
public function quadratic(int $a, int $b, int $c): string
{
// build: x = (-b ± sqrt(b^2 - 4ac)) / 2a
// ... use the builder API, append nodes ...
return $this->mathml->render($root);
}
}
Inspect the real node method names in vendor/ before writing this — the fluent API's exact method spellings are the one thing you cannot guess, and they have changed between minor versions.
Cache aggressively. MathML output is a pure function of its inputs. If formulas are derived from user data, hash a canonical key (expression + parameters) and Cache::remember() the markup string. Rendered MathML is small but tree-building is allocation-heavy; for a page of 50 formulas it's a measurable win.
Store the expression, not the markup. Persist a structured representation (a JSON AST, or a LaTeX-ish source string plus parser) in your model and render on output. Storing raw <math> in the DB locks you into one library version and makes restyling or migration painful.
Blade integration. MathML is markup and must be emitted unescaped: {!! $formula !!} or the directive above. Prefer a Blade component (<x-math :expr="$q->expression" />) over a raw directive so the escaping decision lives in one audited place.
API responses. MathML is well-behaved inside JSON — return { "expression": "...", "mathml": "<math>...</math>" } and let the SPA inject it with innerHTML. This keeps formula rendering server-authoritative while the rest of the app stays a SPA.
Testing. Don't snapshot the entire output string; assert on structure (assertStringContainsString('<mfrac>', $xml)) or, better, simplexml_load_string() the result and walk it. Full-string snapshots break on whitespace and attribute-order changes between patch releases.
The bundle will not boot in Laravel, and 4.0.0 makes that worse. It now requires PHP 8.2 and a Symfony 6.4/7 stack. Laravel pins its own Symfony components (10.x on Symfony 6.4, 11.x on Symfony 7), so pulling in a bundle that wants a specific Symfony generation is a fast route to a composer update conflict or an unwanted component bump. If you must install it, scope it to require-dev, expect twig/twig and symfony/framework-bundle to land in your lock file, and never let it into require.
Maintenance signal is mixed, not dead. 4.0.0 disproves the "hasn't moved in years" read for the bundle — Symfony 7 support is a real, current-generation change. But that release says nothing about mercurysolutions/phpmathml, which is the library you'll actually depend on. Check the library's own tags and commit history before adopting; a maintained wrapper around a stalled core still leaves you exposed to MathML Core drift. Pin an exact version ("mercurysolutions/phpmathml": "1.x.y"), commit composer.lock, and assume you may need to fork. Community reach is still effectively zero — no downstream validation to lean on.
PHP 8.2 floor is a hard gate for older apps. If you're on Laravel 9/10 with PHP 8.1, the bundle's 4.x line won't resolve at all. Irrelevant if you follow the "library only" advice, but it will bite anyone who copies a Symfony 7-era integration snippet into an older app.
MathML Core has dropped things you may reach for. <mfenced> and <maction> are not in Chromium's implementation; don't emit them and expect portability. Also avoid <semantics>/<annotation-xml> if you need consistent cross-browser behaviour — keep to math, mrow, mi, mn, mo, mfrac, msup, msub, msubsup, msqrt, mroot, mtable/mtr/mtd, mtext, mspace.
Escape user input before it touches the builder. If an expression string is user-supplied and you build nodes by interpolation, you've built an XSS vector. Node builders that take text content should escape on their side, but verify — then decide deliberately whether to allow user-supplied <mtext> at all.
PDF generation is the classic wall. dompdf, mpdf, and most Laravel PDF stacks do not render MathML. If the same content must appear in a PDF, plan a second render path now (LaTeX → SVG, or a headless Chromium via Browsershot) rather than discovering it late.
Accessibility. Add alttext on <math> (or an adjacent <span class="sr-only"> with the spoken form). Screen readers handle MathML inconsistently; a plain-language fallback is cheap insurance and often a compliance requirement.
Extension point worth stealing from the bundle: the Twig extension pattern. Mirror it as a single MathMlRenderer service that owns "node tree → string + accessibility attributes + caching." Everything else in your app should call that one class — it's the seam you'll swap if you migrate off the package, and the reason a 0-star dependency doesn't have to be a 0-star risk.
How can I help you explore Laravel packages today?