ezsystems/templated-uri-router
A Symfony URL generator that outputs RFC 6570 templated URIs, letting you build links with placeholders like /demo{?page,sort*,filter*} from route parameters. It's a small, naive implementation that covers basic needs only.
Read the README's usage block and stop there — that's the entire surface area. The package exposes one class, Hautelook\TemplatedUriRouter\Routing\Generator\Rfc6570Generator, whose constructor takes a Symfony RouteCollection and a RequestContext, and whose generate($name, $parameters) returns a string like /demo{?page,sort*,filter*}. That is the whole API.
Do not attempt to install this into a Laravel app. Two hard blockers:
symfony/routing (2.x era) and type-hints Symfony\Component\Routing\RouteCollection / RequestContext. Laravel's Illuminate\Routing\RouteCollection is an unrelated class — there is no adapter, and the package is archived with zero dependents, so none will appear.hautelook/templated-uri-router) does not match the repo (ezsystems/...); the ezsystems copy is a fork. The bundled Symfony integration lives in a separate, also-abandoned bundle repo.Leverage the idea, not the artifact. RFC 6570 URI templates are genuinely useful for HATEOAS/JSON:API link blocks — you emit /demo{?page,sort*,filter*} and let the client fill in what it cares about. Implement that in Laravel with a maintained expander:
composer require rize/uri-template
use Rize\UriTemplate;
$template = (new UriTemplate)->expand('/demo{?page,sort*,filter*}', [
'page' => 2,
'sort' => ['name', 'created_at'],
'filter' => ['active'],
]);
// /demo?page=2&sort=name&sort=created_at&filter=active
guzzlehttp/guzzle ships an equivalent (GuzzleHttp\UriTemplate\UriTemplate::expand) if you already depend on it.
First use case: a paginated index endpoint that returns templated links instead of hard-coded URLs, so a client can request ?page=3&sort=asc without the server knowing which combination it wants.
Bind the expander as a singleton and wrap it behind an interface. This is the one place where copying the original package's design is worth it — keep the caller unaware of which library expands the template.
// app/Support/UriTemplates.php
namespace App\Support;
use Rize\UriTemplate;
class UriTemplates
{
public function __construct(private UriTemplate $tpl = new UriTemplate) {}
/** Build a template from a named Laravel route. */
public function forRoute(string $name, array $queryVars = [], array $seeds = []): string
{
$route = \Route::getRoutes()->getByName($name);
$path = '/'.ltrim(explode('{', $route->uri())[0], '/');
$vars = array_map(
fn ($v) => is_array($v) ? $v.'*' : $v, // * = explode for arrays
$queryVars,
);
$template = $path.($vars ? '{?'.implode(',', $vars).'}' : '');
return $seeds ? $this->tpl->expand($template, $seeds) : $template;
}
public function expand(string $template, array $vars): string
{
return $this->tpl->expand($template, $vars);
}
}
Pattern 1 — hypermedia link block on collections. The direct analogue of the README example:
return response()->json([
'data' => $users,
'links' => [
'self' => "/api/users?page={$users->currentPage()}",
'next' => $templates->forRoute('users.index', ['page', 'sort', 'filter'], ['page' => $users->currentPage() + 1]),
'search' => $templates->forRoute('users.index'),
],
]);
The search link stays unexpanded — the client owns it.
Pattern 2 — route key / resource links. Give models a templatedLink() method that returns the canonical resource URI plus a templated self, so a single collection response supports both fixed and discoverable navigation.
Pattern 3 — API middleware that rewrites links. If you already emit self/next/prev via an ApiResource or JsonResource, a thin TemplatedLinksMiddleware (or a response()->macro) can convert the next entries into templates in one place instead of editing every controller.
Pattern 4 — Blade/@templatedRoute directive for server-rendered pages that hand a template to a JS client (e.g. an Alpine or htmx front-end that appends sort/filter params). Store the template in a data- attribute, expand client-side.
Pattern 5 — contract testing. Assert that a template expands to a URL your real route accepts. That's the only reliable guard against {?page} drifting from {page?}.
$this->get((new UriTemplate)->expand(
route('users.index').'{?page}', ['page' => 2]
))->assertOk();
{?x} vs {&x} is the classic error. The first emits ?, the second emits & and is only legal when a query string already exists. Starting a template with {&page} produces &page=2 with a leading ampersand.* (explode) changes array semantics. {?sort} with ['a','b'] yields sort=a,b; {?sort*} yields sort=a&sort=b. Laravel's $request->query('sort') reads the second form as an array and the first as a string — pick deliberately and parse consistently.expand('/x{?page}', []) returns /x with no ?. That's usually what you want, but it means you cannot distinguish "no page requested" from "empty template" once expanded. Empty-string values are not dropped — they produce page=.{+var} reserved expansion is what you need for path params containing slashes (e.g. a file path or an encoded ID). Simple {var} percent-encodes the slash and breaks route matching. Laravel's route parameter regex (where()) runs against the expanded URL, so a stray encoded slash triggers a 404 you'll spend an hour on.{param?}; RFC 6570 uses {?param}. They are not interchangeable and the derivation in forRoute() above is deliberately crude — it only handles trailing optional segments. For anything more complex, declare the template string explicitly next to the route definition and assert they agree in a test.<a href="/demo{?page}"> is a dead link. They are for machine clients and for JS that calls expand(). Server-rendered navigation needs concrete URLs.['page' => '{page}'] as route parameters — be aware it bypasses Symfony/Laravel's requirement and URL-encoding rules. That hack is why the README says "extremely naive, and incomplete." Reimplement from scratch rather than translating its quirks.Route::getRoutes()->getByName() is the expensive part, not the expansion.How can I help you explore Laravel packages today?