◆ Symfony 6.4 / 7 / 8 bundle · PHP 8.3+ · MIT

Flash messages that keep your pages
cacheable.

$this->addFlash() quietly opens the PHP session, and a page that touches the session can never be stored by Varnish or your CDN. This bundle takes the toast out of the session so your anonymous pages can stay in the cache.

$composer require marilenarm/turbo-toast-bundle

The cost of a flash

One flash message.
Zero cache hits.

The session lock is paid once per request, whatever opens the session. On an otherwise stateless page, a flash message is often the only thing that opens it.

Symfony reacts by marking the response private, so Varnish or your CDN drops it. The session lock serializes concurrent requests, so your lazy Turbo Frames stop loading in parallel. And a session cookie gets created just to say “Item saved.”

Take the flash out and, if nothing else touches the session, the page never opens one. Symfony then has no reason left to mark it private.

Cacheable

Full-page cache, intact

The message rides a Turbo Stream or a cookie instead of the page body, so Varnish and your CDN keep serving the page.

Parallel

Frames stop serializing

Without the session lock, concurrent requests stop queuing behind each other, and several lazy Turbo Frames can load at once.

Stateless

No cookie for a notification

A route you meant to keep stateless no longer gets a session cookie because it said “Item saved”.

Two transports, no session

The toast leaves the session by one of two doors.

Which one depends only on what your controller returns: a Turbo Stream or a full-page redirect.

Turbo Stream

Rendered in the same response

For Turbo and AJAX flows. The toast is generated and rendered inside the very response you return, appended to the DOM, then auto-dismissed by a small Stimulus controller. Nothing is redirected, nothing is stored.

return $this->toast('Item saved');

The API

Pick the verb that matches your response.

Use the trait in any controller. The verb mirrors the response you return, and nothing is inferred behind your back.

You returnYou call
a Turbo Stream responsetoast()  /  toasts()
a RedirectResponse (or any full page)deferToast() before returning
Turbo flow — one toast, one line
use MarilenaRM\TurboToastBundle\Controller\TurboToastTrait;
use MarilenaRM\TurboToastBundle\Toast\Toast;

final class ItemController extends AbstractController
{
    use TurboToastTrait;

    #[Route('/items', methods: ['POST'])]
    public function create(): Response
    {
        // … persist
        return $this->toast('Item saved');
        // or: ->toast('Oops', 'error');
        // or: ->toast('Coming soon', 'info', delay: 8000);
        // or several in one stream:
        // ->toasts(new Toast('Saved'), new Toast('Email sent', 'info'));
    }
}
Redirect flow — deferred over a cookie
#[Route('/login', methods: ['POST'])]
public function login(): Response
{
    // … authenticate
    $this->deferToast('Welcome back!');

    return $this->redirectToRoute('dashboard');
}

// Several at once: one deferToast() call per toast,
// all carried by the same cookie.
▸ Try it here: same CSS and same role as the bundle, and a toast goes away when you click it or wait. For redirects, custom hooks and the profiler panel, open the runnable demo app ↗.

Be honest with your profiler

Worth it on some pages, pointless on others.

It only helps where the flash was the last thing forcing a session open. Everywhere else, keep addFlash() if you like it.

✓ Where it shines

  • Anonymous, cacheable pages behind a CDN, such as catalogs or content sites.
  • Stateless forms using stateless CSRF (Symfony 7.2+).
  • Lazy-frame-heavy pages that shouldn’t serialize on a lock.

✗ Where it gains you nothing

  • Authenticated pages, since the firewall loads the token from the session anyway.
  • Classic session-based CSRF protection.
  • Locale, cart, or anything else already living in the session.

Profile first (in Blackfire, look for session_start and serialized concurrent requests), then decide.

In the box

The edge cases are already handled.

📊

Profiler panel

Every toast emitted per request, per transport, with queued / transported / discarded counts. It is only wired in debug, so production doesn’t pay for it.

♿

Accessible

The aria-live="polite" container survives Turbo Drive. Error toasts render role="alert" (interrupting); every other type gets role="status".

🛡️

XSS-safe by default

Cookie text is treated as untrusted and rendered with textContent only, so no markup in it ever executes.

🧩

Composable streams

Include the toast partial in your own *.stream.html.twig to append a row and notify in one response.

⚙️

No config drift

DOM id, cookie name and Stimulus identifiers are injected from PHP into the markup, so YAML and JS can’t diverge.

🎨

Own the rendering

Swap the template target, or cancel the append event to hand rendering to your own toast library.

🚫

5xx-safe cookie

A request that ended in a server error discards its queued toasts instead of announcing “success” on the next page.

✅

Tested & hardened

PHPUnit + Vitest suites, CI on Symfony 6.4, 7.4 and 8.x and Symfony UX 2 and 3 across PHP 8.3 to 8.5, plus hardening notes that explain each protection.

Get started

Four steps
to no session.

Symfony Flex registers the bundle for you. The rest is one Twig call and, if you like, one CSS import.

Require the package

composer require marilenarm/turbo-toast-bundle

Register the bundle

Automatic with Flex. Otherwise add it to config/bundles.php.

Drop the container in your layout

{# templates/base.html.twig, inside <body> #}
{{ turbo_toast_container() }}

Import the styles (optional)

/* assets/styles/app.css */
@import '~@marilenarm/turbo-toast/styles/toast.css';
Configuration (every value has a default, you only override what you need)
# config/packages/marilena_rm_turbo_toast.yaml
marilena_rm_turbo_toast:
    target: toasts            # DOM id of the container
    default_delay: 5000       # auto-dismiss (ms), 0 to disable
    cookie_name: turbo_toast   # cookie used by deferToast()
    controller_name: marilenarm/turbo-toast/toast
    stream_template: '@MarilenaRMTurboToast/toast.stream.html.twig'