gushphp/to-inline-style-email-bundle
A Symfony bundle that inlines CSS in HTML email templates for better rendering across email clients. It wraps the to-inline-style-email library and adds Symfony service and configuration integration, helping prevent stripped or ignored styles in Gmail, Outlook, and other clients.
First, the honest framing: this is a Symfony 2 bundle, not a Laravel package. It exists to (a) register TijsVerkoyen\CssToInlineStyles\CssToInlineStyles in the Symfony DI container and (b) hook it into the mailer so outgoing messages get their <style> blocks inlined before send. In a Laravel app you get the same translator for free — tijsverkoyen/css-to-inline-styles is a direct dependency of illuminate/mail, and Laravel's Markdown mailables already run it over the rendered HTML.
So the "getting started" path for a Laravel dev is:
# Do NOT require the bundle. Confirm the translator is already present:
composer show tijsverkoyen/css-to-inline-styles
If it isn't (very old Laravel, or you've pruned it):
composer require tijsverkoyen/css-to-inline-styles:^2.2
Where to look first: vendor/tijsverkoyen/css-to-inline-styles/src/CssToInlineStyles.php. The whole API is essentially:
$inliner = new \TijsVerkoyen\CssToInlineStyles\CssToInlineStyles();
$html = $inliner->convert($htmlString, $cssString); // $css optional
First use case: a Blade mailable whose markup references a stylesheet you'd rather not hand-inline. Render the view, feed it through convert(), and set the result as the body:
class InvoiceShipped extends Mailable
{
use Queueable, SerializesModels;
public function build()
{
$html = view('emails.invoice-shipped', ['order' => $this->order])->render();
$css = file_get_contents(resource_path('css/email.css'));
return $this->subject('Your order shipped')
->html((new CssToInlineStyles())->convert($html, $css));
}
}
If you're actually maintaining a Symfony 2/3 app that depends on this bundle, the install path is the classic one: composer require gushphp/to-inline-style-email-bundle, register it in AppKernel::registerBundles(), then check DependencyInjection/ in the bundle source for the service ID and any strip_original_styles / cleanup style config keys before wiring it into your mailer.
1. Lean on Markdown mailables instead. For any new email work, php artisan make:mail Foo --markdown=emails.foo gives you CSS inlining, a responsive table layout, and theming for free. Publish and edit the theme rather than writing an inliner call:
php artisan vendor:publish --tag=laravel-mail
# resources/views/vendor/mail/html/themes/default.css
This is the pattern that makes the bundle redundant for 80% of cases.
2. Trait for classic Blade mailables. If you have an existing fleet of non-Markdown mailables, put the inlining in one place:
trait InlinesStyles
{
protected function inlineHtml(string $view, array $data = [], ?string $css = null): string
{
$html = view($view, $data)->render();
$css ??= file_get_contents(resource_path('css/email.css'));
return (new CssToInlineStyles())->convert($html, $css, 'UTF-8');
}
}
Then ->html($this->inlineHtml('emails.receipt', compact('order'))) in build().
3. Event-listener approach (closest analogue to the bundle's mailer hook). If you want inlining applied globally without touching every Mailable — which is what the Symfony bundle does via mailer events — listen for MessageSending:
// app/Listeners/InlineEmailStyles.php
public function handle(MessageSending $event): void
{
$message = $event->message;
if (! $message->getBody()->hasHtmlBody()) {
return;
}
// Extract <style> blocks, run convert(), replace the HTML part.
}
Register it in EventServiceProvider. The caveat is real: parsing and rewriting the HTML part of a Symfony\Component\Mime\Email is fiddlier than doing it at render time, and you must not clobber the text part.
4. Inline before queueing. Do the conversion inside build() rather than in a sending callback. Queued mailables serialize the built message; if you inline late, a worker re-runs the CSS read (and any file_get_contents failure surfaces in the queue instead of at dispatch). Build once, freeze the HTML.
5. Reuse for previews. The same convert() call powers a dev-only preview route — useful for eyeballing what Gmail will actually receive:
Route::get('/dev/mail-preview/{mailable}', fn ($mailable) => ...);
Gate it behind App::environment('local').
6. Symfony bundle usage (if you're genuinely on Symfony). Inject the inliner service into your mailer/controller and call convert() on the rendered Twig output; the bundle's value-add is the container wiring, not new behavior. Check the bundle's Resources/config/services.* for the exact ID and whether it also registers a Swift Mailer plugin.
7. Keep the media queries. Don't inline everything. The standard pattern is: inline the base styles, leave @media blocks and :hover/pseudo-selectors in a <style> tag in <head>, since those cannot be expressed as inline attributes.
Abandoned since 2017. Last release was 2017-06-17, targeting PHP 5.x / Symfony 2.x. On a modern Laravel it will fail dependency resolution (composer why-not gushphp/to-inline-style-email-bundle will show the Symfony constraint conflict). Zero dependents on Packagist is a strong signal nobody maintains the integration.
Don't double-inline. Laravel's Markdown pipeline already inlines. Running convert() over Markdown-rendered output is wasted CPU and can mangle the carefully-tuned theme output. Detect and skip: if you're using --markdown, don't call the trait.
Version pinning is the trap. Laravel pins a compatible tijsverkoyen/css-to-inline-styles range for illuminate/mail. Don't composer require an arbitrary newer major "just because" — you'll break Markdown mailables. Use composer why tijsverkoyen/css-to-inline-styles to see the constraint and stay inside it.
@media and pseudo-classes are silently lost. They can't be inlined, so if your CSS is only in a <style> block, the inliner drops them. Ship both: inline base rules plus a <style> head block for the responsive overrides.
Specificity inversion. Inlining promotes every rule to element-level, which flips the cascade: a rule you expected a <style> block to override may now lose. Write email CSS defensively — flat selectors, minimal !important.
Encoding. Pass 'UTF-8' explicitly as the third argument. Without it, non-ASCII content (accents, emoji, currency) can come back mojibake'd.
Malformed CSS throws. convert() can raise on bad CSS. It runs at render/send time, so an unrelated typo in a shared stylesheet takes down every email. Wrap the call, log, and fall back to the un-inlined HTML rather than 500-ing a queued job.
Cleanup option. The library's setCleanup() / setStripOriginalStyleTags() options let you remove original <style> tags post-conversion. Don't strip them if you kept media queries — you'll delete your responsive rules.
Extension points. Bind your own configured instance in a service provider so options live in one place:
$this->app->singleton(CssToInlineStyles::class, function () {
return (new CssToInlineStyles())->setCleanup(false);
});
Testing. Assert on the outgoing message, not the view: Mail::fake() + Mail::assertSent(InvoiceShipped::class, fn ($m) => str_contains($m->render(), 'style="')). Cheaper smoke test for "did inlining survive a refactor."
Alternative libraries if you hit limits: pelago/emogrifier (more aggressive, handles more CSS), or the managed route (Premailer/Postmark's inlining API) if you'd rather not own it.
How can I help you explore Laravel packages today?