marilenarm/turbo-toast-bundle
Composer 安装命令:
composer require marilenarm/turbo-toast-bundle
包简介
Session-free flash/toast notifications for Symfony that keep your pages HTTP-cacheable, via Turbo Streams and a cookie transport.
关键字:
README 文档
README
Flash messages that keep your pages HTTP-cacheable.
A page that touches the session cannot be stored by a shared HTTP cache — Symfony
marks it private, and Varnish or your CDN will never serve it. The classic
$this->addFlash() does exactly that: it drags the session (and its lock) into
otherwise stateless pages, just to display "Item saved". One flash message and
your anonymous, perfectly cacheable page becomes uncacheable.
This bundle takes the flash out of the session entirely, with two transports:
- Turbo Stream (AJAX/Turbo flows): the toast is generated and rendered in the same response, appended to the DOM, then auto-dismissed by a small Stimulus controller. No redirect, no storage at all.
- Short-lived cookie (classic full-page redirects): deferred toasts are
serialized into a cookie at
kernel.responsetime and consumed client-side by the container's Stimulus controller on the next page load. The redirected-to page's HTML stays generic — and stays cacheable.
What you get on session-free pages:
- Full-page caching behind Varnish/CDN keeps working, flash messages included — the message travels next to the page (cookie), not inside it.
- Real parallelism: concurrent requests (e.g. several lazy Turbo Frames) no longer serialize on the PHP session lock.
- Stateless routes stay stateless: no session cookie is ever created just to show a notification.
When (not) to use it
Be honest with your profiler before adopting this. The session lock is paid once per request, whatever opens the session:
- Authenticated pages (firewall loads the token from the session), classic
session-based CSRF, locale or cart in session: the session opens anyway, so
removing flashes from it gains you nothing — keep
addFlash()there if you like it. - Anonymous, cacheable pages (catalogs, content sites behind a CDN, stateless forms with stateless CSRF, lazy-frame-heavy pages): this is where the bundle shines — flashes were the last thing forcing a session, and now nothing does.
Profile first (Blackfire: look for session_start and serialized concurrent
requests), then decide.
Requirements
- PHP >= 8.3, Symfony 7.x
symfony/ux-turboandsymfony/stimulus-bundle
Installation
composer require marilenarm/turbo-toast-bundle
Register the bundle (Flex does it automatically):
// config/bundles.php return [ // ... MarilenaRM\TurboToastBundle\MarilenaRMTurboToastBundle::class => ['all' => true], ];
Add the toast container to your base layout:
{# templates/base.html.twig, inside <body> #}
{{ turbo_toast_container() }}
The function renders the container div from the bundle configuration — DOM id,
cookie name and Stimulus identifiers are injected from PHP, so YAML changes can
never drift apart from the JS side. The rendered markup is data-turbo-permanent
(existing toasts survive Turbo Drive navigations) and aria-live="polite"
(inserted toasts are announced by screen readers).
Note
Controllers auto-registered from UX packages get namespaced Stimulus
identifiers: marilenarm--turbo-toast--toast and
marilenarm--turbo-toast--toast-container. If you need full control over the
container markup, write the div manually and keep its Stimulus values in sync
with the bundle configuration yourself.
Import the styles (optional — override freely):
/* assets/styles/app.css */ @import '~@marilenarm/turbo-toast/styles/toast.css';
Usage
Use the trait in any controller:
use MarilenaRM\TurboToastBundle\Controller\TurboToastTrait; final class ItemController extends AbstractController { use TurboToastTrait; #[Route('/items', methods: ['POST'])] public function create(Request $request): Response { // ... persist return $this->toast('Item saved'); // or: $this->toast('Oops', 'error'); // or: $this->toast('Coming soon', 'info', delay: 8000); } }
Emit several at once:
use MarilenaRM\TurboToastBundle\Toast\Toast; return $this->toasts( new Toast('Profile updated'), new Toast('A confirmation email has been sent', 'info'), );
Compose the toast with other streams (append a row and notify) by including the
partial in your own *.stream.html.twig:
<turbo-stream action="append" target="items"> <template>{{ include('item/_row.html.twig', { item: item }) }}</template> </turbo-stream> {{ include('@MarilenaRMTurboToast/toast.html.twig', { message: 'Item saved', type: 'success', delay: 5000, controller: 'toast', }) }}
Not in a controller? Inject MarilenaRM\TurboToastBundle\Toast\ToastRenderer and call
->render(new Toast(...)).
Classic redirects (non-Turbo flows)
When a flow performs a full-page RedirectResponse (post-login redirect, OAuth or
payment callbacks, data-turbo="false" links, locale switch...), there is no Turbo
Stream to render. Use deferToast() instead: the toast is transported by a
short-lived cookie and displayed on the next page load — still no session.
#[Route('/login', methods: ['POST'])] public function login(): Response { // ... authenticate $this->deferToast('Welcome back!'); return $this->redirectToRoute('dashboard'); }
Two explicit verbs, no magic:
| You return | Use |
|---|---|
| a Turbo Stream response | toast() / toasts() |
a RedirectResponse (or any full page) |
deferToast() before returning |
toast() throws a LogicException when the current request does not accept
Turbo Streams (no text/vnd.turbo-stream.html in the Accept header) — a
stream rendered there would reach the browser as raw markup. Turbo forms send
that header automatically; for anything else, use deferToast().
How the cookie transport behaves:
- serialized at
kernel.responsebyToastCookieSubscriber(SameSite=Lax,Secureon HTTPS, notHttpOnly— the JS must read it); the response is forcedprivateso theSet-Cookienever enters a shared HTTP cache; - consumed and cleared before rendering by the
toast-containercontroller (initial load, everyturbo:load, and non-Turbo navigations), so Turbo cache restores never replay a toast; - rendered with
textContentonly — the cookie is client-modifiable, treat it as untrusted display text and never put sensitive data in it; - capped at ~3.8 KB url-encoded; trailing toasts beyond the budget are dropped;
- never set on 5xx responses: a request that ended in a server error discards its queued toasts instead of promising success on the next page.
Each of these addresses a concrete failure scenario — see the security & hardening design notes for the full threat model and the reasoning behind every protection.
Configuration
# config/packages/marilena_rm_turbo_toast.yaml marilena_rm_turbo_toast: target: toasts # DOM id of the container controller_name: marilenarm/turbo-toast/toast # stimulus_controller() notation default_delay: 5000 # auto-dismiss (ms), 0 to disable stream_template: '@MarilenaRMTurboToast/toast.stream.html.twig' cookie_name: turbo_toast # cookie used by deferToast() across redirects
If this bundle saved your pages from the session lock, you can buy me a coffee ☕
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 3
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-07