krystal-sf/ux-paths
A Laravel package for defining, organizing, and resolving UX paths, helping you manage user-flow routes and navigation links consistently across your application. Lightweight, framework-friendly helpers keep path logic centralized and easy to maintain.
krystal-sf/ux-paths is a Symfony UX library — "Intelligent Paths for Krystal Symfony Projects." Before you do anything, understand the constraint: this is a Symfony bundle, not a Laravel package. There is no UxPathsServiceProvider and no config/ux-paths.php. You'll use it either (a) inside a Symfony app, or (b) by pulling out its path/value objects into a Laravel service layer.
Minimal steps:
composer require krystal-sf/ux-paths
Then, in a Symfony host app:
// config/bundles.php
return [
// ...
Krystal\Ux\Paths\KrystalUxPathsBundle::class => ['all' => true],
];
In Laravel, you can't auto-register the bundle. Install it for its classes and wire the resolver yourself:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->singleton(\Krystal\Ux\Paths\PathResolver::class, function ($app) {
return (new \Krystal\Ux\Paths\PathResolver(...))
->setRouter($app['router']); // adapt to the real constructor
});
}
Where to look first:
README.md and CHANGELOG.md — the API surface is small; the README is usually the spec.src/ — look for the Path value object and the resolver/service class. Those two tell you 90% of what the package does.config/services.yaml (if present) — shows service IDs, aliases, and injectable contracts.tests/ — treat tests as executable documentation; they reveal edge-case behavior (trailing slashes, optional params, locale suffixes).First use case: stop hand-concatenating route strings. Replace '/users/' . $user->id . '/edit' with a resolved path from the package:
$path = $resolver->resolve('user_edit', ['id' => $user->id]); // -> "/users/42/edit"
Use that immediately in navigation menus, breadcrumbs, and redirect targets — that's where hardcoded paths rot fastest.
new itThe whole value of the package is that routing/context/config is centralized. Inject the resolver (Symfony autowiring, or a Laravel singleton + facade). Anything that constructs a path by hand defeats the point.
final class BreadcrumbBuilder
{
public function __construct(private PathResolver $paths) {}
public function forProfile(User $user): array
{
return [
['label' => 'Home', 'href' => $this->paths->resolve('home')],
['label' => 'Users', 'href' => $this->paths->resolve('user_index')],
['label' => $user->name, 'href' => $this->paths->resolve('user_show', ['id' => $user->id])],
];
}
}
If the package exposes a Path (or similar) object, keep it typed all the way through your domain/controller layer and cast to string only at the view boundary ((string) $path, {{ $path }}). This keeps normalization (leading slash, encoding, trailing slash) in one place.
$path = $this->paths->path('user_show', ['id' => $user->id]);
return redirect((string) $path);
The "intelligent" part is usually a configurable mapping layer. Define path definitions once (YAML in Symfony, PHP array in Laravel) and reference them by logical key:
# config/packages/ux_paths.yaml (Symfony)
krystal_ux_paths:
definitions:
dashboard: { route: app_dashboard }
user_edit: { route: user_edit, params: [id] }
// config/ux-paths.php (Laravel equivalent, consumed by your wrapper)
return [
'definitions' => [
'dashboard' => ['route' => 'dashboard'],
'user_edit' => ['route' => 'users.edit', 'params' => ['id']],
],
];
This is the main extension point: when the package's naming doesn't fit your app, wrap it — don't fork it.
In Symfony, register/consume the Twig extension the bundle ships (typically a path()-like function). In Laravel, you have two clean options:
@uxpath('user_edit', ['id' => $user->id]).ux_path()) that resolves from app(PathResolver::class).Avoid calling app() from views directly if you can — inject into view composers instead.
If the package supports context (locale, host, base path), pass it explicitly and test it. This is where Laravel and Symfony diverge most: Symfony's RequestContext lives in symfony/routing, which Laravel also depends on but doesn't use identically. Verify the resolver reads the context you expect — don't assume app()->getLocale() propagates.
$resolver->setContext(['locale' => app()->getLocale(), 'host' => request()->getHost()]);
Assert semantic behavior in your tests:
$this->assertSame('/users/42/edit', (string) $this->paths->path('user_edit', ['id' => 42]));
If you're on Laravel, run these in a single resolver test suite — they're pure-PHP, no HTTP kernel needed. That's a strong argument for adopting the package even in a Laravel codebase.
PackageManifest won't see it, php artisan vendor:publish won't find it, and its config/services.yaml won't load. Budget time for a thin wrapper class in app/Support/. Do not try to shoehorn the bundle's DI into Laravel's container.bind() / singleton() explicitly. Missing bindings are the #1 "why does it say Target [PathResolver] is not instantiable" error.users.edit in Laravel, user_edit in Symfony typically). A typo surfaces as an unresolvable path — assert on route existence in a test (Route::has(...)) rather than letting it break a page at runtime./ or ?. Encoding behavior should be uniform across every caller — that's exactly what the value object buys you.composer.json require/conflict blocks against your Laravel version's symfony/* pins. If it requires symfony/ux-* you may end up dragging in a lot of transitively required, unused packages. Consider composer why symfony/ux-twig-component.bin/console cache:clear after edits, or you'll debug stale paths for an hour. In Laravel, if you load definitions from config, php artisan config:clear is your friend during development.bin/console debug:container Krystal and bin/console debug:router. Laravel: php artisan tinker → app(PathResolver::class)->resolve(...), plus Route::getRoutes() to confirm names. Wrap the resolver in a small interface so you can swap in a fake during testing.vendor/. Custom path resolvers (for tenant-prefixed URLs, preview environments, etc.) should implement the package's interface and be swapped via your container binding. Same for Twig/Blade helpers — register your own, don't patch the bundle's."krystal-sf/ux-paths": "1.2.*") and add a resolver-level test suite on day one — you are your own support channel.route() — you probably don't. The value appears once you have many logical path definitions (nav, breadcrumbs, sitemaps, canonical URLs) and want one authority for them.How can I help you explore Laravel packages today?