hautelook/templated-uri-router
A router that matches and generates URLs using RFC 6570 URI templates, so routes can be defined with named placeholders instead of fixed segments. Extends zend-http routing to assemble spec-compliant URLs flexibly.
This package is a Symfony routing add-on, not a Laravel package. Before investing time, confirm which of the two you actually need:
/users/42) — use Laravel's route()/url() helpers or Symfony's UrlGenerator. You do not need this package./users/{id}, /users{?page,limit}) — reach for this package when you are building a hypermedia/HATEOAS API where the client is expected to fill in variables, not the server.Minimal setup:
composer require hautelook/templated-uri-router
Where to look first, in order:
README.md — confirms the supported RFC 6570 operators ({var}, {+var}, {?var}, {&var}, {/var}, {.var}, {;var}, {#var}).src/ — find the class implementing Symfony's UrlGeneratorInterface. That is your entry point. Expect something like a TemplatedUrlGenerator wrapping a RouteCollection plus a compiler.tests/ — the test fixtures show exactly which route shapes produce which template strings. This is the fastest way to learn the mapping rules; treat the tests as the real documentation.composer.json — check the symfony/routing version constraint so you know which RouteCollection/Route API you are coding against.First use case — an API endpoint returning link templates:
// Pseudocode around the generator's actual API; verify method names in src/
$template = $generator->generate('users.show'); // "/users/{id}"
$template = $generator->generate('users.friends'); // "/users/{id}/friends{?page,limit}"
Return that inside your resource representation:
{
"id": 42,
"name": "Ada",
"_links": {
"self": "/users/{id}",
"friends": "/users/{id}/friends{?page,limit}"
}
}
If you are 100% Laravel and have no Symfony routing layer, stop here and read the Gotchas section — bridging Laravel's Illuminate\Routing\RouteCollection into a Symfony RouteCollection is possible but manual, and usually not worth it for a 2-star, low-activity dependency.
1. Do not wire it into url(). Treat it as a separate, explicit service. Laravel's helpers generate concrete URLs; this generates templates. Mixing the two in one codebase is fine, but never let one call the other implicitly.
final class LinkTemplateBuilder
{
public function __construct(private TemplatedUrlGenerator $generator) {}
public function for(string $route, array $params = []): string
{
return $this->generator->generate($route, $params);
}
}
2. Compose with your API Resource / transformer layer. Templates belong next to the resource they describe, not scattered through controllers. A simple pattern is a per-resource links() method that enumerates route names:
public function links(User $user): array
{
return [
'self' => $this->templates->for('users.show'),
'friends' => $this->templates->for('users.friends'),
'edit' => $this->templates->for('users.edit'),
];
}
3. Treat templates as static and cache them. A route definition does not change between requests, so the generated template string does not either. Compute once per route name (or build a full route-name → template map at boot in a service provider) and reuse. Do not recompute per record in a collection serializer.
// Build once
$map = [];
foreach ($routeNames as $name) {
$map[$name] = $generator->generate($name);
}
4. Use a real RFC 6570 library on the client to expand. The server emits the template; the consumer expands it. Use rize/uri-template in PHP tests, or any RFC 6570 library in JS, to prove that /users/{id}/friends{?page,limit} expands to /users/42/friends?page=2&limit=10. This is how you unit-test the contract without round-tripping through the router.
5. Keep a hand-written route list as the source of truth. Do not derive templates from user input or from dynamically registered routes. Enumerate the route names you want exposed, so a new route is a deliberate decision, not an accidental leak.
6. Bridge Laravel route definitions only if you must. Laravel routes are backed by Illuminate\Routing\Route, not Symfony Route. To use the generator you would have to construct a Symfony RouteCollection and add a Route per named Laravel route:
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
$collection = new RouteCollection();
foreach (Route::getRoutes() as $laravelRoute) {
if ($name = $laravelRoute->getName()) {
$collection->add($name, new Route($laravelRoute->uri()));
}
}
Do this in a dedicated adapter class and keep it behind an interface. That way, if you replace the dependency with hand-rolled templates, only one file changes.
This is not Laravel-native. There is no service provider, no config file, no facade. You provide the RouteCollection and the generator yourself. Budget one adapter class, not a drop-in install.
Route requirements (regex) do not survive templating. A Symfony route with requirements: ['id' => '\d+'] becomes /users/{id} — the client loses the constraint. Document variable formats out of band or use {id} plus a description in your API docs.
Optional parameters change the operator. A trailing optional parameter may come out as {/id} (path segment expansion) rather than /{id}. Inspect the tests for the exact rule before building client code against it.
Encoding differences. The templating encoder is deliberately not the same as Symfony's URL encoder. {+var} reserved expansion and {var} differ in what gets percent-encoded. Do not assume output is interchangeable with rawurlencode.
Unnamed or missing routes throw, quietly or loudly. Verify what happens for a route name that does not exist — usually a RouteNotFoundException. Wrap calls and fail fast at boot rather than per-request.
Host and scheme-level templates are the risky part. Subdomain variables ({sub}.example.com) and scheme/host components in route definitions are the least likely to be handled correctly. Test them explicitly or keep templates path-only.
Pin the version and vendor a fallback. Two stars, zero dependents, last release 2023. Use an exact version constraint ("hautelook/templated-uri-router": "x.y.z"), and keep your LinkTemplateBuilder interface thin enough that you can swap the implementation for a hand-written template map in an afternoon.
Debugging recipe. When a template looks wrong, dump the underlying Symfony Route and compare against its compiled representation:
$route->compile()->getVariables(); // what the router thinks is variable
$route->compile()->getTokens(); // how it was tokenized
If a segment you expected to be a placeholder came out literal, it is almost always a requirement, default, or an unresolvable variable — the compiled tokens will tell you which.
Do not mix generators in one response. If part of your payload uses concrete URLs from route() and part uses templates, clients cannot tell which is which. Pick one strategy per endpoint and document it.
Extension point. The generator's variable-handling layer (whatever turns compiled tokens into {...} expressions) is the natural place to customize if your API needs a specific operator (e.g., always {?a,b} for query parameters). Subclass rather than patching vendor code.
How can I help you explore Laravel packages today?