PHPackages                             marilenarm/turbo-toast-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. marilenarm/turbo-toast-bundle

ActiveSymfony-bundle[Mail &amp; Notifications](/categories/mail)

marilenarm/turbo-toast-bundle
=============================

Session-free flash/toast notifications for Symfony that keep your pages HTTP-cacheable, via Turbo Streams and a cookie transport.

v0.2.0(2d ago)00MITPHP &gt;=8.3

Since Jul 7Compare

[ Source](https://github.com/marilenaRM/turbo-toast-bundle)[ Packagist](https://packagist.org/packages/marilenarm/turbo-toast-bundle)[ Fund](https://ko-fi.com/marilenarm)[ RSS](/packages/marilenarm-turbo-toast-bundle/feed)WikiDiscussions Synced today

READMEChangelog (6)Dependencies (14)Versions (8)Used By (0)

TurboToastBundle
================

[](#turbotoastbundle)

**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.response` time 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
--------------------

[](#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](https://symfony.com/blog/new-in-symfony-7-2-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
------------

[](#requirements)

- PHP &gt;= 8.3, Symfony 7.x
- `symfony/ux-turbo` and `symfony/stimulus-bundle`

Installation
------------

[](#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  #}
{{ 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
-----

[](#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`:

```

    {{ include('item/_row.html.twig', { item: item }) }}

{{ 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)

[](#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 returnUsea 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.response` by `ToastCookieSubscriber` (`SameSite=Lax`, `Secure` on HTTPS, not `HttpOnly` — the JS must read it); the response is forced `private` so the `Set-Cookie` never enters a shared HTTP cache;
- consumed and **cleared before rendering** by the `toast-container` controller (initial load, every `turbo:load`, and non-Turbo navigations), so Turbo cache restores never replay a toast;
- rendered with `textContent` only — 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 &amp; hardening design notes](docs/hardening.md) for the full threat model and the reasoning behind every protection.

### Customizing cookie-rendered toasts

[](#customizing-cookie-rendered-toasts)

Two hooks, both keeping the message XSS-safe (`textContent` only):

**Template** — write the container manually (instead of `turbo_toast_container()`) and put a `` target inside it. Its root element is cloned per toast, receives the `toast--{type}` class and the delay value; the message lands in the `[data-toast-message]` element (or the root when none is declared):

```

```

**Event** — take over rendering entirely by cancelling the `marilenarm--turbo-toast--toast-container:append` event:

```
document.addEventListener('marilenarm--turbo-toast--toast-container:append', (event) => {
    event.preventDefault();
    myToastLibrary.show(event.detail.toast.message, event.detail.toast.type);
});
```

Profiler
--------

[](#profiler)

In debug mode, a **Turbo Toast** panel appears in the Symfony profiler: every toast emitted during the request, per transport (Turbo Stream / cookie), with the queued / transported / discarded counts for the cookie path. Zero overhead outside `kernel.debug` — the traceable decorators are only wired there.

Configuration
-------------

[](#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](https://ko-fi.com/marilenarm) ☕

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

6

Last Release

2d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/d5eb0cb636095d2c3f2301c21ceaa3df17b19bdb5c009c395aa359b3d34d1911?d=identicon)[marilenaRM](/maintainers/marilenaRM)

---

Tags

symfonybundlenotificationsflashstatelesstoastturbohotwirehttp-cachestimulus

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/marilenarm-turbo-toast-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/marilenarm-turbo-toast-bundle/health.svg)](https://phpackages.com/packages/marilenarm-turbo-toast-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M389](/packages/easycorp-easyadmin-bundle)[symfony/security-bundle

Provides a tight integration of the Security component into the Symfony full-stack framework

2.5k185.6M2.4k](/packages/symfony-security-bundle)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.9M750](/packages/sylius-sylius)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.6M582](/packages/shopware-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
