edinchev/flash-alert-bundle
A Laravel bundle for rendering flash alert messages in your application. Provides a simple way to queue and display success, error, warning, and info notifications across redirects and requests, with Blade views for consistent, customizable alert output.
Reality check first: edinchev/flash-alert-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/flash-alert.php, and Composer auto-discovery will do nothing for you. So "getting started" splits into two tracks depending on what you're actually doing.
composer require edinchev/flash-alert-bundle
app/AppKernel.php → registerBundles().config/bundles.php:
Edinchev\FlashAlertBundle\FlashAlertBundle::class => ['all' => true],
composer.json — tells you which Symfony versions are actually supported. This is your go/no-go gate.Resources/views/ — the Twig template you'll {% include %} in your base layout.Resources/public/js/ — the pure-JS renderer. Read it before trusting it; it defines the DOM contract and any globals it expects.Services/ or FlashAlert.php) — this is your public API surface.// Controller
$this->get('flash_alert')->success('Saved.'); // or constructor-inject the service
return $this->redirectToRoute('dashboard');
{# base.html.twig #}
{% include 'FlashAlertBundle::flash.html.twig' %}
Plus the JS/CSS from the bundle's Resources/public directory wired into your asset pipeline.Don't install it. The bundle's value is roughly 150 lines of logic plus a small DOM renderer, and all of it is trivially reproducible in Laravel with session()->flash() and any toast library (Notyf, Toastr, or a 40-line Alpine component). The only reason to open the repo is to steal the integration pattern, not the code.
Equivalent Laravel skeleton:
// app/Support/Flash.php
class Flash
{
public static function success(string $msg): void { session()->flash('flash', ['type' => 'success', 'message' => $msg]); }
public static function error(string $msg): void { session()->flash('flash', ['type' => 'danger', 'message' => $msg]); }
}
{{-- resources/views/partials/flash.blade.php --}}
@if (session('flash'))
<div class="alert alert-{{ session('flash.type') }}" data-flash>{{ session('flash.message') }}</div>
@endif
That is the whole package, in a form you can debug.
The bundle exposes a service (flash_alert in Symfony 2/3 style). In modern Symfony you constructor-inject it; in Laravel terms this is exactly like injecting a FlashService instead of calling session() from twelve places.
final class OrderController
{
public function __construct(private FlashAlert $flash) {}
public function store(StoreOrder $request)
{
$order = Order::create($request->validated());
$this->flash->success("Order #{$order->id} created.");
return $this->redirectToRoute('orders.index');
}
}
Why this matters day-to-day: it gives you one seam to intercept. In a Laravel port, that same seam lets you swap in a queue-backed or Livewire-broadcast flash without touching controllers.
The single most common failure mode with any flash package is double rendering: the layout includes the partial, and an AJAX-loaded fragment also includes it. Define the contract explicitly:
{# Only in the base layout, guarded so partials can opt out #}
{% block flash %}{% include 'FlashAlertBundle::flash.html.twig' %}{% endblock %}
{{-- Laravel equivalent: guard with a view-share flag --}}
@unless($__env->yieldContent('suppressFlash'))
@include('partials.flash')
@endunless
The bundle ships with the usual success / info / warning / error types. Resist adding a fifth one until you've checked that your CSS and JS actually handle it — the renderer typically has a hardcoded class map.
enum FlashLevel: string {
case Success = 'success';
case Error = 'danger'; // note: Bootstrap uses 'danger', some bundles use 'error'
case Warning = 'warning';
case Info = 'info';
}
Nail down the string contract between PHP and JS early. A mismatch between error and danger produces a message that renders with no styling at all — silently, in production, and it looks like "the package is broken."
Flash messages are almost always paired with a redirect. Encapsulate the pair:
protected function backWith(string $type, string $message): RedirectResponse
{
return redirect()->back()->with('flash', compact('type', 'message'));
}
In Symfony-land the same idea is a trait or an abstract controller method calling $this->flash->{$type}() before the redirectToRoute.
If your frontend does partial reloads, the flash has to survive the boundary. Two viable approaches:
[data-flash] nodes on DOMContentLoaded. Cheap, but doesn't work for XHR responses that never touch a layout.fetch(url, {headers: {'X-Requested-With': 'XMLHttpRequest'}})
.then(r => r.json())
.then(({flash}) => flash && window.FlashAlert.show(flash.type, flash.message));
The bundle's pure-JS renderer is the useful part here — it's framework-free, so this porting path is the least painful.
Whatever you build, assert on the session payload, not on rendered HTML:
$this->post('/orders', $data)->assertSessionHas('flash.type', 'success');
// Symfony
$this->assertTrue($session->getFlashBag()->has('success'));
If you find yourself asserting on CSS classes, your test is coupled to the renderer — which is the piece most likely to change or need replacing.
It's Symfony-only, and there's no adapter. No FlashAlertServiceProvider, no Flash facade, no publishable config. If you composer require this into a Laravel app you'll get an autoloaded class or two that reference Symfony SessionInterface and AbstractBundle types. Depending on your Symfony components version in Laravel, some of it may load — which is worse than a hard failure, because it creates the illusion of a supported integration.
Zero stars, zero dependents, zero recent activity is the headline risk. For a package this small the risk isn't bugs, it's abandonment: it will pin to old Symfony versions and block your major upgrades. If you adopt it in Symfony, fork it or vendor the ~3 files you actually use. For a Laravel project this is a firm "don't — write the 40 lines yourself."
Flash semantics differ between the frameworks. Symfony's FlashBag is a bag of arrays keyed by type — getFlashBag()->add('success', '...') and multiple messages per type. Laravel's session()->flash() stores a single value per key unless you use ->push(). If you're porting, you need session()->push('flash', [...]) or a namespaced array, otherwise your second message on the same request silently overwrites the first.
Peek vs. get. Symfony's getFlashBag()->get('success') consumes the messages; peek() does not. A layout that renders flashes on both an error page and a base.html.twig extension of it will show them twice — or zero times. Laravel's session('flash') is non-consuming for the current request, so the analogue is "renders twice on the same request but vanishes on the next." Both are confusing; test the redirect()->back() path specifically, because that's the one users actually hit.
Escaping. The renderer is vanilla JS and may inject into innerHTML. Verify whether the bundle escapes message content. If it doesn't, and you're building messages from user input ("Invite sent to {$email}"), you have an XSS vector. In Laravel/Blade, {{ }} saves you by default — don't switch to {!! !!} to "fix" a broken message.
Asset pipeline is the real setup cost. Resources/public is not served from a web-accessible path in Symfony. You need assets:install (or a Webpack Encore alias), plus the file gets re-copied on every composer install. In Laravel terms this is the vendor:publish step — except there's nothing doing it for you automatically. Budget more time here than for the PHP side.
CSS framework mismatch. The generated markup will carry classes for whichever CSS the bundle was authored against (Bootstrap 3-era is common for packages like this). Under Tailwind or Bootstrap 5 those class names either do nothing or conflict. The fix is one template override — check whether the bundle supports template overriding via Resources/views in your app, and if not, copy the template into your own namespace and include that instead.
Ordering is not guaranteed. Symfony's FlashBag preserves insertion order per type, but a naive renderer that loops types in a fixed array order will group all successes before all errors regardless of when they occurred. If ordering matters (it usually doesn't), flatten to a single ordered list instead of a type-keyed map.
Translation. Messages are pre-rendered strings by the time they reach the renderer, so |trans has to happen at flash time, not display time. Flash a translation key instead if you have a multi-locale app — otherwise a user who switches language mid-session sees the old locale's message. (Low priority, but it's a one-way door: changing it later means touching every controller.)
Extension point to prefer. If you do adopt the bundle, the cleanest customization seam is a custom Twig template that keeps using the bundle's service and skips its markup entirely. That way the message vocabulary and the flash-bag handling stay consistent, and you're only replacing the ~15 lines of HTML/JS you disagree with. Avoid subclassing the service unless the bundle was designed for it — small packages rarely mark their internals final correctly, and you'll inherit the maintenance burden either way.
Verdict for a Laravel developer: read the repo for the pattern, skip the dependency. The genuinely reusable idea is "a tiny service wrapping session flash + one guarded layout partial + a framework-agnostic JS renderer you control." Everything else is framework glue you'd rewrite anyway — and rewriting 40 lines is cheaper than carrying an unmaintained Symfony dependency into a Laravel app.
How can I help you explore Laravel packages today?