◆ Symfony 7 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 anonymous pages stay fast, parallel, and fully cacheable.
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 of the session and the page needs no session at all. Nothing forces it uncacheable anymore.
The message travels next to the page, not inside it — carried by a stream or a cookie. Varnish and your CDN keep serving.
With no session lock, concurrent requests stop queuing behind each other — several lazy Turbo Frames can finally load at once.
No session cookie is created just to show a notification, and that was the last thing still opening one.
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. No redirect, no storage at all.
The API
Use the trait in any controller. The verb you pick mirrors the response you return — 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; 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); } }
#[Route('/login', methods: ['POST'])] public function login(): Response { // … authenticate $this->deferToast('Welcome back!'); return $this->redirectToRoute('dashboard'); } // Several at once, any transport: $this->toasts( new Toast('Profile updated'), new Toast('Email sent', 'info'), );
role, dismissed by a click anywhere or by waiting, exactly like the bundle. The runnable demo app ↗ exercises every flow — redirects, custom hooks, profiler:
Be honest with your profiler
It only helps where the flash was the last thing forcing a session. 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. Zero overhead outside debug.
The aria-live="polite" container survives Turbo Drive. Error toasts render role="alert" (interrupting); every other type gets role="status".
Cookie text is rendered with textContent only and treated as untrusted — no markup ever executes.
Include the toast partial in your own *.stream.html.twig — 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 — no false “success” on the next page.
PHPUnit + Vitest suites, CI on PHP 8.3 & 8.4, and a documented threat model behind every 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'