
r/symfony

I built a SPA router for SonataAdmin (Symfony) that requires zero backend changes — @wlindabla/sonata_spa [OSS]
Reddit — r/symfony + r/typescript + r/webdev
Hey r/symfony,
Long-time SonataAdmin user here. I finally got fed up with full page reloads on every navigation and built a proper fix.
TL;DR: @wlindabla/sonata_spa is a TypeScript library that turns your existing SonataAdmin backend into an SPA — sidebar clicks, pagination, filters, sorting, show/delete — all handled without full reloads. Zero changes to your Symfony controllers or Twig templates.
yarn add @wlindabla/sonata_spa
> Requires: AdminLTE >= 3.x / 4.x · Bootstrap >= 5.3 · SonataAdmin >= 4.x · Node.js >= 18
Why I built this
SonataAdmin is genuinely excellent. But by default, every navigation is a full page reload — every script re-executes, every Bootstrap 5 component and every Stimulus controller re-initializes from scratch. For an internal admin panel used daily by your team, this friction is real.
Most "fixes" either:
- Replace Sonata entirely with a React frontend (lose everything Sonata gives you)
- Use Turbo (conflicts with Sonata's Stimulus outlet system —
uniqid()IDs change on every request) - Patch individual pages with custom AJAX (unmaintainable mess)
None of these are satisfying. I needed something that preserves 100% of Sonata's features while eliminating full reloads.
The architecture — Symfony concepts in TypeScript
The library is directly inspired by Symfony's HttpKernel and EventDispatcher:
User clicks link
→ BindingManager builds SpaRequest
→ SpaKernel.handle(request)
1. dispatch spa:request ← STOPPABLE
2. RequestMatcher — server-managed? → full reload if yes
3. RouteResolver.resolve(url) → RouteMatch
4. dispatch spa:route:resolved ← STOPPABLE
5. dispatch crud:list / crud:show / crud:delete / ...
→ Subscriber: fetch → DOM swap → history → Stimulus reconnect → dom:ready
Minimal setup
// assets/spa.ts
import { SpaKernel, SpaEvents } from '@wlindabla/sonata_spa';
import { BrowserEventDispatcher } from '@wlindabla/event_dispatcher/browser';
document.addEventListener('DOMContentLoaded', () => {
const spa = SpaKernel.create(
{
router: {
sidebarSelector: '#app-sidebar',
mainSelector: '#app-main',
mainContentAreaSelector: '#app-content',
mainContentHeaderSelector: '#app-content-header',
},
},
'prod',
new BrowserEventDispatcher(window, { bubbles: true })
);
// Re-initialize your libraries after each swap
spa.getDispatcher().addListener(SpaEvents.DOM_READY, (event) => {
initMyLibraries(event.container);
});
spa.boot();
});
{# In your standard_layout.html.twig #}
{% block javascripts %}
{{ parent() }}
{{ encore_entry_script_tags('spa') }}
{% endblock %}
That's the full setup for basic usage.
Key features
- Surgical DOM swaps — only the changed parts of the page are replaced. List pages swap only the data table + filters. Show/dashboard pages swap the full
#app-main. - Correct Stimulus reconnection —
DomManagerdetects new Stimulus outlet IDs after each swap and forces reconnection viarequestAnimationFrame. - CSRF-aware delete flow — fetches the Sonata delete confirmation page, extracts the CSRF token, then POSTs with
X-Requested-With: XMLHttpRequest. SweetAlert2 modal instead of browserconfirm(). - Two-step batch flow — POSTs the batch form, parses the Sonata confirmation HTML, shows a SweetAlert2 modal, then re-POSTs with
confirmation=ok. - History API —
pushStatewithRouteMatchstored in the state object. Popstate (back/forward) replays navigation without re-resolving the URL. - Extension system — modeled after Sonata's
AdminExtensionInterface. Add custom route patterns, binding managers, subscribers, and server-managed URL rules without touching the sealed kernel. - Full TypeScript — strict types,
exactOptionalPropertyTypes: true.
Override the default confirmation UI
// Replace SweetAlert2 with your own modal — priority > 0 overrides the default
dispatcher.addListener(
SpaEvents.DELETE_CONFIRM_REQUESTED,
async (event) => {
const confirmed = await myModal.confirm(event.title, event.message);
if (confirmed) {
await event.accept(); // proceeds with DELETE POST
} else {
event.cancel();
}
},
10 // higher priority than the built-in default (0)
);
What stays server-managed (full reload)
By design, /create and /edit URLs always trigger a full reload. These pages require server-side CSRF token generation and Sonata's session management. Form submissions are handled via SPA (JSON response), but the initial page load is always a full request.
You can add more server-managed patterns in the options:
serverManagedUrlOptions: [
/\/export(\?.*)?$/,
/\/import(\?.*)?$/,
]
Links
- npm:
yarn add @wlindabla/sonata_spa - GitHub: https://github.com/Agbokoudjo/sonata_spa
- Author: AGBOKOUDJO Franck (internationaleswebservices@gmail.com)
- License: MIT
Happy to answer questions about the implementation — especially around the Stimulus reconnection logic and the CSRF flow, which were the trickiest parts to get right.
Built with ❤️ in Benin 🇧🇯 — INTERNATIONALES WEB APPS & SERVICES
Composer 2.9.8 and 2.2.28 fix GitHub Actions token disclosure in error messages
Please immediately update Composer to version 2.9.8 or 2.2.28 (LTS) by running composer.phar self-update. The new releases fix a vulnerability where Composer leaks the full contents of GitHub Actions issued GITHUB_TOKENs or GitHub App installation tokens to the GitHub Actions logs. GitHub introduced a new format for these tokens including a - (hyphen). The new format is gradually being rolled out to repositories. The new format fails Composer’s validation, leading to an error message that exposes the full token contents to stderr. A CVE identifier will be assigned and added to this post once available.
What are your favorite Claude Code skills?
Hey devs,
What are your favorite Claude Code skills?
Whether it's for quality audits, security audits, refactoring, testing, or anything else — I'd love to hear what you've found useful in your Symfony workflow.
Feel free to share links to repos or your own custom skills.
Thanks!
FormsBundle - Release
I've always had a distaste for writing `FormType` classes, so much so that I just stopped using them. But then I had to deal with raw HTML forms, and I hated that even more, so I decided to automate the boring part.
FormsBundle generates Symfony forms directly from DTOs: property types, nullability, and validator constraints already encode everything a `FormType` needs, the bundle just reads them, builds the form type class, and caches it.
Link: https://github.com/n-fasano/FormsBundle
Feedback welcome!
AssetMapper seems to not detect changes for CSS when using Docker (dunglas)
I use Dunglas symfony docker to run the app (on dev environment)
Installed asset mapper, everything is setup correctly by flex.
I update CSS file but the version of css exposed to browser stays the same and therefore no changes are reflected. Anyone had this issue?
I remember using simple symfony serve it worked fine.
[Showcase] OrderInvoiceBundle: Symfony bundle to manage orders and invoices without full e-commerce overhead.
If you don't need a full-blown e-commerce store but just need to handle orders and generate invoices (PDF, proforma, etc.) in your Symfony app, I've created a bundle to simplify this: https://github.com/DavidPetrasek/OrderInvoiceBundle
----------------------------------------------
I am aware Sylius provides standalone Components & Bundles like https://github.com/Sylius/SyliusOrderBundle
... but they're outdated and can't find them in their new docs. Sylius also doesn't seem to support invoice types: regular, proforma, advance and final