symfony/ux-router
Symfony UX Router brings Symfony routing to JavaScript, letting you generate URLs from route names and parameters, match current routes, and navigate client-side. It integrates with Turbo and Stimulus to eliminate hardcoded URLs in dynamic Symfony apps.
This release is notable less for the changelog entries than for what they represent: feature #1234 Add Symfony UX Router package ships the router as a brand-new Symfony UX package, versioned in lockstep with the rest of the UX family. Treat v3.6.0 as a 0.x-quality API in a 3.x version number — nothing in the changelog is a breaking change, but there is no prior assessment to inherit, so this is a greenfield evaluation.
First, calibrate expectations: symfony/ux-router is a Symfony bundle. It assumes FrameworkBundle, the Symfony Routing component, and (usually) AssetMapper or a Stimulus setup. There is no Laravel service provider, no config stub, and no facade. In a Laravel codebase you are not "installing a package you use"; you are borrowing a route-dumping strategy and a small JS runtime, then wiring the Laravel side yourself.
Minimal steps to evaluate it:
composer require symfony/ux-router in a scratch app (not your Laravel app first) so you can see the vendor tree without polluting composer.json.vendor/symfony/ux-router/README.md and the src/ directory before the docs site — confirm the dump command name, the on-disk format of the generated routes, and how the JS entry point is exposed. This is the single most important step; the JS contract is what you'd be reimplementing.First real use case: replacing hardcoded URLs in JS (fetch calls, Stimulus/Vue controllers, Alpine handlers) with name-based route generation, so that a Route::name('orders.show') rename doesn't silently break the front end. If that is your only goal, Ziggy gets you there in five minutes; ux-router is worth the effort mainly if you already share routing semantics with a Symfony service or are building a hybrid app.
Pattern 1 — Symfony-native (the intended path). Enable the bundle, run the package's route dump as part of the container build, and import the generated routing module into your Stimulus controllers. Route generation at runtime is a lookup into the dumped table, so there is no HTTP round-trip and no server-side rendering of URLs. The container-build hook is what bug #4082 fixed, so the dump now happens reliably even when no dump directory is configured — meaning the dump is a build artifact you should treat like compiled assets, not like a runtime cache.
Pattern 2 — Laravel adapter via an Artisan dump command. The pragmatic integration. Write a command that walks Route::getRoutes() and emits the JSON shape that ux-router's JS expects, then alias it into your deploy pipeline:
// app/Console/Commands/DumpJsRoutes.php
foreach (Route::getRoutes() as $route) {
if (! in_array('GET', $route->methods(), true)) {
continue;
}
$routes[$route->getName() ?? $route->uri()] = [
'path' => '/'.ltrim($route->uri(), '/'),
'tokens' => $route->parameterNames(),
// map Laravel's where() constraints onto ux-router's requirements shape
];
}
Then run it from post-autoload-dump in composer.json alongside @php artisan package:discover so the file is always regenerated on composer install and composer update. That single decision removes the entire class of "stale routes in CI" bugs.
Pattern 3 — Vite/build integration. Import the package's JS entry point directly from vendor/ (or copy it into resources/js/vendor/ if your Vite config can't resolve outside node_modules) and feed it your dumped table. Keep the generated file in .gitignore; regenerate in the build script so a checkout plus npm run build always produces matching artifacts.
Pattern 4 — Locale-aware routes. Feature #4076 ("fall back to the application default locale") is the pattern that will affect you most: routes with a locale prefix no longer require an explicit locale argument. Mirror it by injecting config('app.locale') into your dumper and emitting it as the fallback locale in the generated config. Be explicit about this — the fallback is resolved at dump time, not request time.
Pattern 5 — Incremental adoption. Because the generated table is plain data, you can start by exporting only a whitelist of route namespaces (orders.*, api.*) and leave the rest of the front end on hardcoded paths. This keeps the diff small and makes the eventual removal of Ziggy (or hardcoded URLs) mechanical.
config/packages/*.yaml conventions, and AssetMapper assumptions do not transfer. Budget for the adapter layer; do not budget for a php artisan vendor:publish.php artisan route:cache and php artisan config:cache interact with it: if your JS routes are dumped before the route cache is rebuilt, you ship last deploy's routes. Order your deploy steps explicitly (route cache → JS dump → asset build) and assert the output file's mtime in CI.php artisan route:list --json against your generated table before trusting the whole set.node -e "console.log(require('./routes.json'))" beats adding console.log inside a bundled controller.resources/js/generated/ so Vite hot-reload picks up changes), and the JS wrapper (a thin url(name, params) function of your own keeps the package swappable if you later migrate to Ziggy or drop it entirely).How can I help you explore Laravel packages today?