Native PHP port of MJML — the markup language for responsive HTML emails.
MJML-PHP converts MJML markup into responsive HTML that works across all major email clients, including Outlook. No Node.js dependency required.
- PHP 8.2+
ext-domext-libxml
composer require shyim/mjml-phpuse Mjml\Mjml;
$result = Mjml::render('<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text>Hello World</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>');
echo $result->html;use Mjml\Mjml;
use Mjml\MjmlOptions;
$result = Mjml::render($mjml, new MjmlOptions(
keepComments: true,
minify: false,
beautify: false,
language: 'en',
dir: 'ltr',
));
echo $result->html;use Mjml\Mjml;
$mjml = new Mjml();
$mjml->registerComponent(MyCustomComponent::class);
$result = $mjml->toHtml('<mjml>...</mjml>');A small dependency-free CLI is exposed as Composer bin mjml-php:
vendor/bin/mjml-php email.mjml -o email.html
vendor/bin/mjml-php < email.mjml > email.htmlUseful options:
vendor/bin/mjml-php email.mjml --validation-level=skip
vendor/bin/mjml-php email.mjml --process-includes --include-path=partialsRun vendor/bin/mjml-php --help for all options.
mj-body— Email body containermj-section— Horizontal section with background image/color support and Outlook VMLmj-column— Responsive column with auto-width distributionmj-group— Non-responsive column groupingmj-wrapper— Section wrapper with gap support
mj-text— Styled text blockmj-image— Responsive image with srcset/sizes and fluid-on-mobilemj-button— Call-to-action buttonmj-divider— Horizontal rulemj-spacer— Vertical spacingmj-table— HTML table passthroughmj-raw— Raw HTML passthrough
mj-accordion— Expandable/collapsible sections (CSS-only, no JavaScript)mj-carousel— Image carousel (CSS radio-button technique)mj-navbar— Navigation bar with responsive hamburger menumj-hero— Full-width hero section with VML background for Outlookmj-social— Social media icons (17 built-in networks)
mj-title— Email titlemj-preview— Preview textmj-attributes— Default attribute values andmj-classdefinitionsmj-font— Web font importsmj-style— Custom CSS (inline or in<style>tag)mj-breakpoint— Mobile responsive breakpointmj-html-attributes— Custom HTML attributes via CSS selectors
MJML-PHP validates your markup and throws a ValidationException on errors:
use Mjml\Validation\ValidationException;
use Mjml\Validation\ValidationLevel;
try {
$result = Mjml::render($mjml);
} catch (ValidationException $e) {
echo $e->getMessage();
foreach ($e->errors as $error) {
echo $error; // "Line 5: Attribute 'colr' is not allowed on mj-text (mj-text)"
}
}- Strict (default) — Validate and throw
ValidationExceptionon errors - Soft — Validate but do not throw; errors are exposed via
$result->errors - Skip — No validation
$result = Mjml::render($mjml, new MjmlOptions(
validationLevel: ValidationLevel::Soft,
));
foreach ($result->errors as $error) {
error_log((string) $error);
}All library exceptions extend Mjml\MjmlException, so you can catch them with a single catch:
use Mjml\MjmlException;
use Mjml\Parser\ParseException;
use Mjml\Validation\ValidationException;
try {
$result = Mjml::render($mjml);
} catch (ValidationException $e) {
// Markup-level validation failures
} catch (ParseException $e) {
// Circular includes, broken mj-include references, etc.
} catch (MjmlException $e) {
// Any other library failure
}MjmlException extends \RuntimeException, so existing catch (\RuntimeException $e) blocks still work.
Input is trusted. MJML markup is treated as a template authored by you, not as end-user input. Do not concatenate untrusted strings into MJML markup — attribute values flow into the rendered HTML without HTML-escaping, the same as the official JS MJML.
Defensive measures the renderer already applies:
- URL attributes (
href,src,background,action,formaction,poster) pass through a scheme allowlist.javascript:,vbscript:,file:, and similar are rewritten to#. Onlyhttp,https,mailto,tel,sms,ftp,cid, anchor fragments, protocol-relative URLs, and relative paths pass through.data:image/*is allowed for inline images. mj-fontURLs that are nothttp/https/protocol-relative are dropped instead of being emitted into<link>/@import.mj-includeis disabled by default (unlike the JS MJML CLI). When enabled (ignoreIncludes: false), included paths are jailed under the current file's directory plus any explicitincludePathroots, withrealpathresolution, null-byte / URL-encoded-traversal rejection, and circular-include detection.- libxml is invoked with
LIBXML_NONET(no network access). Under PHP 8 / libxml ≥ 2.9, external entities are not resolved by default, so this parser is not vulnerable to XXE.
If you must interpolate user data into MJML, escape it yourself before passing it to the renderer (htmlspecialchars for text content; URL-encode parameters you put into href query strings).
This is a native PHP port aligned with MJML 5.2.1. The HTML output is tested against the original JavaScript implementation using snapshot tests to ensure identical rendering. A CI job re-renders the snapshot fixtures with mjml@5.2.1 and fails on drift, so if the upstream JS package publishes a patch you may see CI failures — open an issue and regenerate the fixtures.
- CSS
@importinlining: The CSS inliner does not resolve@importdirectives found in inline style blocks. This matches the behavior of the JS MJML reference implementation for email-safe output. - CSS shorthand parsing: Only
padding,margin, andbordershorthands are fully supported for width calculation. More exotic shorthand properties (e.g.,border-radiuswith/syntax) are passed through as-is.
# Install dependencies
composer install
# Run tests
vendor/bin/phpunit
# Run only snapshot tests (compares against JS MJML output)
vendor/bin/phpunit --testsuite SnapshotSmall render and validation cases can be added as .test files under tests/Fixtures/.
Use labeled sections:
--OPTIONS--
validationLevel: skip
ignoreIncludes: false
--MJML--
<mjml>...</mjml>
--HTML--
Expected HTML fragment
--HTML-NOT--
Unexpected HTML fragment
--HTML-RAW--
Expected raw multi-line HTML fragment
--HTML-NOT-RAW--
Unexpected raw multi-line HTML fragment
--ERRORS--
Expected validation error fragment
--EXCEPTION--
Mjml\Parser\ParseException
Expected exception message fragment
For simple render fixtures, the legacy shorthand is also supported:
<mjml>...</mjml>
----
Expected HTML fragment
The snapshot test fixtures compare PHP output against reference HTML generated by the JS MJML CLI. To regenerate:
npm install -g mjml
for f in tests/Snapshot/Fixtures/*.mjml; do
npx mjml@5.2.1 "$f" --no-minify > "${f%.mjml}.html"
doneMIT