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

Twigemailrenderbundle Laravel Package

generalmedia/twigemailrenderbundle

Generalmedia TwigEmailRenderBundle is a Symfony bundle for rendering email templates with Twig. It helps streamline building HTML email content in Symfony apps by integrating Twig-based email template rendering.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config publish tag. Before anything else, clone the repo and read, in this order:

  1. src/ (or Service/) — the renderer class itself. It is almost certainly a thin wrapper: Twig\Environment::render($template, $context) plus, at most, CSS inlining and a plain-text pass.
  2. DependencyInjection/*Extension.php + Resources/config/services.yaml — this is where the Symfony container wiring lives and where the real behaviour (template path, cache dir, inliner settings) is configured.
  3. Tests/ — fastest way to see a working call with realistic arguments.

Do not composer require generalmedia/twigemailrenderbundle into a Laravel app. It declares symfony/framework-bundle and will not boot. The practical first use case for a Laravel developer is one of two things:

A. You are sharing email templates with a Symfony service. Then port the renderer (it's ~50 lines) and point both apps at the same Twig template directory.

B. You just want Twig-rendered HTML mail in Laravel. Then use twig/twig directly:

composer require twig/twig
// app/Services/TwigEmailRenderer.php
namespace App\Services;

use Twig\Environment;
use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles;

class TwigEmailRenderer
{
    public function __construct(private Environment $twig) {}

    public function render(string $template, array $data = []): string
    {
        $html = $this->twig->render($template, $data);

        return (new CssToInlineStyles())->convert($html);
    }
}
// app/Providers/TwigServiceProvider.php
public function register(): void
{
    $this->app->singleton(Environment::class, function () {
        $twig = new Environment(
            new FilesystemLoader([
                resource_path('views/emails/twig'),
                base_path('shared-templates/emails'),
            ]),
            [
                'cache'            => storage_path('framework/twig'),
                'autoescape'       => 'html',
                'strict_variables' => ! $this->app->isProduction(),
            ]
        );

        $twig->addExtension(new \App\Twig\LaravelHelpersExtension());

        return $twig;
    });
}

tijsverkoyen/css-to-inline-styles is already a transitive dependency of laravel/framework, so the inliner is free.

Implementation Patterns

Sending from a Mailable. Keep mail transport in Laravel, only the body in Twig. Note build() is not container-resolved, so use app():

class OrderShipped extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public Order $order) {}

    public function build(): static
    {
        $html = app(TwigEmailRenderer::class)->render('emails/order-shipped.twig', [
            'order'    => $this->order->only(['id', 'total', 'shipped_at']),
            'cta_url'  => route('orders.show', $this->order),
            'support'  => config('mail.from.address'),
        ]);

        return $this->subject("Order #{$this->order->id} shipped")
                    ->html($html);
    }
}

Pass scalars and arrays, not Eloquent models — keeps Twig ignorant of your ORM and makes templates reusable from the Symfony side.

Bridge Laravel helpers into Twig. This is the pattern that makes the port actually ergonomic:

class LaravelHelpersExtension extends AbstractExtension
{
    public function getFunctions(): array
    {
        return [
            new TwigFunction('route', fn (string $n, array $p = []) => route($n, $p)),
            new TwigFunction('asset_url', fn (string $p) => asset($p)),
            new TwigFunction('trans', fn (string $k, array $r = []) => __($k, $r)),
        ];
    }
}

Only expose what email templates need. Avoid exposing config() and env() — templates become debugging nightmares.

Layout inheritance for email. Mirror your Blade mail::message layout as a Twig base:

{# emails/layout.twig #}
<!doctype html>
<html><body style="margin:0;padding:0;background:#f4f4f4">
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0">
    <tr><td align="center">
      <table role="presentation" width="600" style="background:#fff">
        <tr><td>{% block content %}{% endblock %}</td></tr>
      </table>
    </td></tr>
  </table>
</body></html>
{% extends "emails/layout.twig" %}
{% block content %}
  <h1>Order #{{ order.id }} shipped</h1>
  <a href="{{ cta_url }}">Track it</a>
{% endblock %}

Local preview route. Indispensable when the designer is writing Twig and you're writing PHP:

Route::get('/dev/email/{template}', function (string $template) {
    abort_unless(app()->isLocal(), 404);

    return app(TwigEmailRenderer::class)
        ->render("emails/{$template}.twig", ['order' => (object) ['id' => 1234, 'total' => '42.00']]);
})->where('template', '[a-z0-9\-/]+');

Snapshot-test the output. Since the template is the product, assert on rendered HTML rather than on mocked mail calls:

it('renders the shipped email', function () {
    $html = app(TwigEmailRenderer::class)->render('emails/order-shipped.twig', [
        'order'   => ['id' => 1, 'total' => '10.00'],
        'cta_url' => 'https://example.test/o/1',
    ]);

    expect($html)->toContain('Order #1 shipped')
                 ->toContain('https://example.test/o/1');
});

Gotchas and Tips

  • The bundle will not autoload services in Laravel. Its Extension class registers definitions with Symfony's ContainerBuilder. Laravel's container has no idea those exist. You are copying the class body, not the package.
  • Twig strict_variables vs Blade's forgiveness. Blade silently renders null for undefined variables; Twig with strict_variables: true throws Twig\Error\RuntimeError. Enable strict only outside production, and use {{ foo|default('') }} for optional fields.
  • Escaping is the inverse of Blade. {{ var }} auto-escapes (like {{ }} in Blade); {{ var|raw }} is {!! !!}. Email HTML is full of & in tracked URLs — do not reach for |raw to fix it, build the URL with http_build_query.
  • Absolute URLs are mandatory. url() inside a queued job resolves against the worker's APP_URL. Set APP_URL correctly on queue hosts, or call URL::forceRootUrl(config('app.url')) in a service provider.
  • @vite does not exist in Twig. There is no asset pipeline for email anyway. Inline CSS into the template or let the inliner do it — but Gmail strips <style> blocks in some clients, so rely on the CssToInlineStyles pass, not <link> tags.
  • Queue workers cache config. After changing Twig paths or APP_URL, run php artisan config:clear and restart workers (queue:restart). Twig's own compiled cache in storage/framework/twig must be cleared on deploy if templates moved.
  • Extension points worth adding before you need them: a trans function (emails are the first thing that gets localized), and a {% apply inline_css %} filter if you want inlining inside the template rather than in PHP. The Symfony bundle likely wires one or both via twig.extension tags — replicate that rather than editing the renderer.
  • Blade components have no Twig equivalent. @component/<x-button> must be rewritten as {% include 'emails/partials/button.twig' with {...} only %}. Use only to avoid leaking the whole context into partials.
  • Zero stars, zero dependents means zero community answers. Pin to a commit hash, and vendor the renderer into app/Services with your own test. If you genuinely need the Symfony bundle's config surface, run git log --oneline and read the last three commits — with this repo age, the HEAD is the documentation.
  • Debugging: Twig has no dd(). Register new TwigFunction('dump', fn ($v) => logger()->debug(print_r($v, true))), or use {% verbatim %}{{ dump(var) }}{% endverbatim %} with the Twig\Extension\DebugExtension enabled locally. The most common failure is a LoaderError from a path that worked in Symfony but isn't in your FilesystemLoader array — check the loader paths first, not the template syntax.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource