◆ Symfony 6.4 / 7 / 8 bundle · PHP 8.3+ · MIT
$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.
The cost of a flash
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.
The message rides a Turbo Stream or a cookie instead of the page body, so Varnish and your CDN keep serving the page.
Without the session lock, concurrent requests stop queuing behind each other, and several lazy Turbo Frames can load at once.
A route you meant to keep stateless no longer gets a session cookie because it said “Item saved”.
Two transports, no session
Which one depends only on what your controller returns: a Turbo Stream or a full-page redirect.
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.
The API
Use the trait in any controller. The verb mirrors the response you return, and nothing is inferred behind your back.
| You return | You call |
|---|---|
| a Turbo Stream response | toast() / toasts() |
a RedirectResponse (or any full page) | deferToast() before returning |
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')); } }
#[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.
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
It only helps where the flash was the last thing forcing a session open. Everywhere else, keep addFlash() if you like it.
Profile first (in Blackfire, look for session_start and serialized concurrent requests), then decide.
In the box
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.
The aria-live="polite" container survives Turbo Drive. Error toasts render role="alert" (interrupting); every other type gets role="status".
Cookie text is treated as untrusted and rendered with textContent only, so no markup in it ever executes.
Include the toast partial in your own *.stream.html.twig to append a row and notify in one response.
DOM id, cookie name and Stimulus identifiers are injected from PHP into the markup, so YAML and JS can’t diverge.
Swap the template target, or cancel the append event to hand rendering to your own toast library.
A request that ended in a server error discards its queued toasts instead of announcing “success” on the next page.
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
Symfony Flex registers the bundle for you. The rest is one Twig call and, if you like, one CSS import.
composer require marilenarm/turbo-toast-bundleAutomatic with Flex. Otherwise add it to config/bundles.php.
{# templates/base.html.twig, inside <body> #} {{ turbo_toast_container() }}
/* assets/styles/app.css */ @import '~@marilenarm/turbo-toast/styles/toast.css';
# 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'