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

Premailer Bundle Laravel Package

wecodepixels/premailer-bundle

Wraps the Premailer library for PHP applications, automatically inlining CSS into HTML emails so they render correctly in Gmail, Outlook, and other clients. Configure stylesheets, base URLs, and link handling, then generate email-ready markup with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

This bundle is a thin Symfony DI wrapper around the premailer library, whose job is to take a rendered HTML email plus its CSS and return the same HTML with those styles moved into style="" attributes (plus a <style> fallback block for clients that support it). It does one thing; the value is that you get it as an autowired service instead of hand-rolling the call in every mailer.

  1. Install and register:
composer require wecodepixels/premailer-bundle

Symfony 4+/5+ Flex-era projects usually don't need manual registration, but if the bundle isn't in config/bundles.php:

// config/bundles.php
return [
    // ...
    WeCodePixels\PremailerBundle\PremailerBundle::class => ['all' => true],
];
  1. Read the bundle before you use it. It is small (a DependencyInjection/ folder, a Resources/config/services.xml or similar, and almost nothing else). Ten minutes in vendor/wecodepixels/premailer-bundle/ will tell you the exact service id, the config tree keys, and which underlying class you're actually injecting. Do this before guessing at YAML keys.

  2. Confirm the runtime dependency is present. Depending on which premailer package the bundle pulls in, it either shells out to the Ruby premailer gem or uses a pure-PHP inliner. Check composer show -t wecodepixels/premailer-bundle and, if Ruby is involved:

which premailer
gem list premailer
  1. Find the service:
bin/console debug:container premailer
bin/console debug:container --show-private | grep -i premail
  1. First use case — an order confirmation:
use Premailer\Premailer; // verify the FQCN from the bundle's services config

final class OrderMailer
{
    public function __construct(
        private Environment $twig,
        private MailerInterface $mailer,
        private Premailer $premailer,
    ) {}

    public function send(Order $order): void
    {
        $html = $this->twig->render('email/order_confirmation.html.twig', [
            'order' => $order,
        ]);

        $premailer = clone $this->premailer;
        $premailer->setHTML($html);
        $premailer->setBaseURL('https://example.com/'); // trailing slash matters

        $inlined = $premailer->getHTML();

        $email = (new Email())
            ->to($order->getCustomer()->getEmail())
            ->subject('Your order')
            ->html($inlined);

        $this->mailer->send($email);
    }
}

If the constructor/API differs (it's a small wrapper, so it may just be convert($html, $baseUrl)), match whatever the bundle exposes — don't fight it.

Implementation Patterns

Decorate the mailer once, not every call site. The cleanest integration is a decorator around MailerInterface, or an event listener on Symfony\Component\Mailer\Event\MessageEvent, that rewrites the HTML body of every outgoing Email. Every template author then gets inlined CSS for free, and you never forget to call premailer on a new email.

final class InliningMailer implements MailerInterface
{
    public function __construct(
        private MailerInterface $inner,
        private Premailer $premailer,
        private string $baseUrl,
    ) {}

    public function send(RawMessage $message, Envelope $envelope = null): void
    {
        if ($message instanceof Email && $html = $message->getHtmlBody()) {
            $p = clone $this->premailer;
            $p->setHTML($html);
            $p->setBaseURL($this->baseUrl);
            $message->html($p->getHTML());
        }
        $this->inner->send($message, $envelope);
    }
}

Register it with decorates: mailer.mailer (or the appropriate mailer service id) and decoration_inner_name.

Centralise in an EmailRenderer. If you don't want a decorator, a single EmailRenderer::render(string $template, array $context): Email service keeps Twig rendering, base-URL handling, and inlining in one place. This is the easiest thing to mock in tests.

Use absolute_url() in templates for assets. The base URL you pass to premailer resolves relative src/href values, but Symfony's Twig absolute_url() is more reliable for assets and links because it uses the request context (or a configured one in CLI/workers). Pick one strategy and stick to it — mixing them produces emails that work in dev and break in queue workers.

Generate the plain-text part too. Premailer operates on HTML only. Either render a parallel email/foo.txt.twig, or use the library's text conversion if it exposes one (getText()/toText() in some versions). Symfony Mailer will not invent a text part for you.

Set the base URL per environment and per tenant:

# config/services.yaml
services:
    App\Mail\InliningMailer:
        arguments:
            $baseUrl: '%env(APP_URL)%'

If you run multi-tenant, inject a request-aware base-URL resolver rather than a static parameter.

Skip work you don't need. For plain notification emails with a single inline style attribute already, the transform is wasted CPU. Gate the decorator on a header or a marker interface.

Test with a fake inliner. Don't shell out in unit tests. Bind a no-op or pass-through implementation of the premailer service in config/services_test.yaml, and add one integration test (in CI only) that asserts a known template produces a known inlined body.

Gotchas and Tips

The Ruby/proc_open dependency is the number one production failure. If the underlying package shells out, proc_open may be disabled on shared hosting, and php-fpm's PATH rarely matches your shell's. Set an absolute path in config if the bundle supports it, and verify from inside a web request, not just from the CLI. Docker images need ruby and the gem installed in the same image as PHP.

Base URL needs a trailing slash and must be the public origin, not the internal hostname. A missing slash gives you https://example.comimages/logo.png and silently broken images in every email.

Media queries are preserved, not inlined. @media rules stay in the <style> block because they can't be expressed as inline attributes. If you use the data-premailer="ignore" attribute on a <style> or <link> element, premailer skips it — useful for client-specific hacks you need to keep intact, dangerous if you typo it and your whole stylesheet is skipped.

Relative URLs and @import are resolved against the base URL at transform time. Anything behind auth, behind a VPN, or served from a non-public CDN will not resolve.

Performance. If each transform spawns a subprocess, this is tens to hundreds of milliseconds per email plus process overhead. Fine for transactional mail at low volume, not fine inside a loop that sends a newsletter to 50k recipients. Move inlining to a worker, cache rendered results where the content is deterministic, and measure before you assume.

Errors are opaque. Subprocess failures often surface as a generic exception with the real message on stderr. Wrap the call, log stderr (and the command line in debug mode), and fail loudly in dev — a silently un-inlined email looks fine in Gmail and broken in Outlook.

Prefer the pure-PHP path if you can. tijsverkoyen/css-to-inline-styles or pelago/emogrifier do the same job without Ruby, are far more widely used, and integrate with Symfony Mailer in a few lines. If this bundle's transport turns out to be painful, swapping in one of those behind your EmailRenderer is a one-file change — which is exactly why you should put the transform behind an interface of your own rather than injecting the bundle's class everywhere.

The bundle has essentially no adoption (zero stars, zero dependents at time of writing). That's not automatically disqualifying for a thin wrapper, but it means: pin the version in composer.json, don't expect fixes upstream, and keep the surface area small. Read the DI config so you can replace it with four lines of services.yaml if it's abandoned.

Extension points are limited to what Symfony gives you: override the service definition, decorate it, or add a compiler pass. There is no plugin system to speak of, so don't plan around one.

Encoding and escaping. Twig autoescapes; premailer does not. Run the transform on the already-rendered HTML and never re-escape the output before passing it to Email::html().

Snapshot the output. Add a test that renders your base layout through premailer and commits the resulting HTML. Email regressions are invisible until a customer complains, and a golden-file test catches a broken <style> tag, a missing base URL, or an accidentally-inlined-but-skipped rule immediately.

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.
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
uni-method/symfony-lib