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

Mjml Php Laravel Package

qferr/mjml-php

mjml-php compiles MJML markup into responsive, email-client-friendly HTML from PHP and Laravel. Build complex transactional emails with reusable components instead of hand-written tables, and render them reliably across inboxes.

Deep Wiki
Context7

Getting Started

Install the package

composer require qferr/mjml-php

Laravel 7+ gets the service provider via package discovery. If you're on an older version or a non-Laravel app, register Qferr\MjmlPhp\MjmlPhpServiceProvider manually, and resolve the renderer from the container rather than using the facade.

Install the rendering dependency. mjml-php is a thin PHP wrapper around the MJML Node.js toolchain — it does not ship a compiled MJML engine. The package shells out to an mjml binary. Before anything works you need Node on the box and MJML available:

# pick one
npm install -g mjml
# or, pinned locally to your project
npm install --save-dev mjml

If you install locally, the binary ends up at ./node_modules/.bin/mjml and you'll need to point the config at it.

Publish and read the config first. This is the single most important file to open before writing any templates:

php artisan vendor:publish --provider="Qferr\MjmlPhp\MjmlPhpServiceProvider"

config/mjml.php holds the binary path (and, depending on version, the node path, timeout, and any extra CLI flags). Verify the exact key names against the published file — they have shifted between releases — and confirm the resolved path actually exists on your deployment target.

First use case. The canonical flow is compile-on-demand for a transactional email:

use Qferr\MjmlPhp\Facades\Mjml;

$html = Mjml::render(<<<'MJML'
<mjml>
  <mj-body>
    <mj-section>
      <mj-column>
        <mj-text>Hello from MJML</mj-text>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>
MJML);

Confirm the facade alias and method name in src/Facades/* — the package exposes a single render/toHtml-style call, and it's worth reading the class body once so you know precisely what it returns and what it throws.

Implementation Patterns

Keep MJML as files, not string literals. Store templates under resources/views/mail/mjml/ as .mjml files and read them in, so editors, diffs, and IDE syntax highlighting behave:

$html = Mjml::render(
    file_get_contents(resource_path('views/mail/mjml/welcome.mjml'))
);

Wrap it once. Don't sprinkle facade calls through controllers and Mailables. Create a single service — App\Mail\MjmlRenderer — that reads the config, injects variables, and returns HTML. Injecting MJML variables is best done with str_replace/Blade-style interpolation on the raw MJML before compilation (MJML itself supports {{ var }} placeholders when you pass --config options, but the PHP wrapper doesn't forward per-render context in all versions, so pre-interpolating is the portable approach).

Use it inside Mailables. The natural integration is to compile in the Mailable's build() (or a content() view) and hand the resulting HTML to Laravel's mailer:

public function build()
{
    return $this->html(app(MjmlRenderer::class)->render('welcome', $this->data))
                ->subject('Welcome');
}

Cache the compiled output. Rendering spawns a Node process — tens to hundreds of milliseconds per template. For templates that change rarely, cache by a hash of the source:

$key = 'mjml:' . md5($source);
return Cache::remember($key, now()->addDay(), fn () => Mjml::render($source));

Bust the cache on deploy or key it off the file's mtime. Never cache templates containing per-recipient dynamic data unless you interpolate after the cache lookup.

Render off the request path for anything heavy. Use a queued job to compile and persist HTML to storage or the DB when templates are large or generated in bulk. This also isolates Node crashes from user-facing requests.

Standardize a layout partial. MJML supports <mj-include>, which reads from disk relative to the compiled file. If you adopt it, make sure the working directory passed to the child process is the directory of the template — otherwise includes silently fail or resolve to the wrong path.

Gotchas and Tips

Node is a hard, silent dependency. The package's failure mode when mjml is missing is often a process exception or an empty string, not a friendly "install Node" message. Add a health check to your deploy pipeline (which mjml / node -v) and a startup assertion that the configured binary is executable.

Version drift between the wrapper and MJML. The last release is from April 2022, so the MJML version it was written against is old. If you install a current mjml globally, newer component syntax may render fine while deprecated tags still work — but the reverse (using a modern MJML feature that the wrapper passes through incorrectly, or CLI flag changes) will bite. Pin the MJML version in your project and upgrade deliberately.

Timeout and memory. Long templates or many includes can exceed the process timeout. Look for a timeout setting in the config; if it isn't exposed, you're relying on Symfony Process defaults. Compile-time failures usually surface as generic process exceptions — log the raw stderr from the exception message, not just "render failed".

Path resolution differs by environment. ./node_modules/.bin/mjml depends on the current working directory. On Forge/Vapor/Docker the cwd at render time may not be your project root, so prefer an absolute path in config, or resolve it via base_path('node_modules/.bin/mjml'). Vapor and other read-only/serverless environments need MJML baked into the image or layer — you cannot npm install at runtime.

Config caching. After php artisan config:cache in production, changes to config/mjml.php are ignored. Re-run the command whenever the binary path changes; a stale cached path is the most common "it worked locally" bug with this package.

Escape interpolated data. MJML is XML — user-supplied values injected before compilation must be escaped, or a stray & or < breaks the document. Interpolating with htmlspecialchars() (or compiling the static shell and injecting into the HTML afterward) avoids corrupt markup.

Exceptions are your contract. Inspect the thrown exception type in the package source and catch it specifically in your wrapper so a failed render falls back to a plain-HTML template instead of a 500 on a password-reset email.

Extension points. Since it's a small wrapper: the facade and the underlying renderer are swappable via the container binding. If you need per-render options (validation level, minification, --config overrides), subclass the renderer or drop the facade and build a small Symfony Process wrapper of your own — the package is thin enough that forking the ~1 class into app/ is a legitimate long-term maintenance strategy given the release cadence.

Test with snapshots. Assert rendered HTML against a committed fixture so an MJML or Node upgrade that changes output (whitespace, inlined CSS) fails loudly in CI rather than shipping broken layouts.

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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core