hautelook/templated-uri-bundle
Symfony bundle that adds support for RFC 6570 URI templates, letting you generate and expand dynamic URIs from route or link templates with named parameters. Useful for hypermedia APIs and consistent link generation.
Install and register
composer require hautelook/templated-uri-bundle:^3.5
Note the naming mismatch you'll hit immediately: the package you install is hautelook/templated-uri-bundle, but the repository is goetas/TemplatedUriBundle. Confirm in composer.lock which namespace actually landed in vendor/ before writing imports — the bundle class is Goetas\TemplatedUriBundle\TemplatedUriBundle in the upstream repo.
As of 3.5.0 the real URI-template work is done by hautelook/templated-uri-router, which this release bumps to ^4.0 (a major bump of the dependency, shipped in a minor bundle release). Confirm the resolved version with composer show hautelook/templated-uri-router before writing anything that touches the lower-level expansion API.
Register it in config/bundles.php:
return [
// ...
Goetas\TemplatedUriBundle\TemplatedUriBundle::class => ['all' => true],
];
Where to look first
TemplatedRouterInterface (and the concrete templated_uri.router service it aliases) — this is the whole public surface of the bundle. If it doesn't do what you need, nothing else in the bundle will save you.composer show hautelook/templated-uri-router — read the installed version and, if you're coming from a pre-3.5 install, the 4.0 changelog. The router is where the actual RFC 6570 expansion lives; the bundle is a thin adapter over it.DependencyInjection/Configuration.php for the config tree — options are minimal, but check for an enabled/replace flag before assuming the router is wired into the container the way you expect.First use case
The canonical value proposition is hypermedia / API discovery: you want to emit a link template into a response so a client can fill in the blanks, instead of guessing URL shapes.
Define a route like any other:
// config/routes.yaml
users_index:
path: /users
controller: App\Controller\UserController::index
Then, instead of $urlGenerator->generate('users_index', ['page' => 2]) which commits you to ?page=2, ask for the template:
use Goetas\TemplatedUriBundle\Routing\TemplatedRouterInterface;
final class UserController
{
public function __construct(private TemplatedRouterInterface $router) {}
public function index(): JsonResponse
{
return $this->json([
'_links' => [
'self' => $this->router->generateTemplated('users_index', []),
// -> /users{?page,limit,sort}
],
]);
}
}
That string is RFC 6570-valid and can be expanded by any conforming client.
Template now, expand later. Keep two call sites separate in your head: concrete URL generation via Symfony's UrlGeneratorInterface when the client already gave you the parameters, and generateTemplated() when you're publishing capabilities. Don't try to use one for the other.
Pagination and filter discovery. The highest-leverage pattern is a resource representation that advertises its own query surface:
$links = [
'self' => $router->generateTemplated('users_index', []),
'next' => $urlGenerator->generate('users_index', ['page' => $page + 1]),
'search' => $router->generateTemplated('users_search', []),
];
Clients get a stable contract for "what can I ask for" without you hard-coding ?page={page}&limit={limit} strings in docs that drift.
Wrap it before you spread it. Maintenance profile is still thin — one 2023 release, then a single-contributor dependency-bump release in 3.5.0. Do not inject TemplatedRouterInterface across twenty controllers. Create one thin application service:
final class LinkTemplateFactory
{
public function __construct(
private TemplatedRouterInterface $router,
private UrlGeneratorInterface $urlGenerator,
) {}
public function template(string $route, array $params = []): string
{
try {
return $this->router->generateTemplated($route, $params);
} catch (\Throwable $e) {
// fall back to a concrete URL so a bad template never 500s a response
return $this->urlGenerator->generate($route, $params);
}
}
}
That gives you a single seam to swap in hautelook/templated-uri-router directly (or your own expansion) if the bundle stops working on a future Symfony.
Twig integration. If you need templates in views, don't rely on a bundle-provided Twig function existing — expose your LinkTemplateFactory as a Twig extension yourself. One class, full control, no dependency on bundle internals.
Testing against the spec, not the bundle. Assert your generated templates expand to the URLs you expect using the same underlying URI-template library the bundle uses. Check the class name in vendor/hautelook/templated-uri-router first — the 4.0 bump may have moved it:
$template = $factory->template('users_index'); // /users{?page,limit}
$expanded = $expander->expand($template, ['page' => 2]); // hautelook/templated-uri-router API
self::assertSame('/users?page=2', $expanded);
This keeps your tests meaningful even if you replace the bundle.
OpenAPI / API docs. This is a clean fit: emit the template string as the path in an OpenAPI operation, or as servers entries, so your generated docs and your runtime hypermedia agree.
Package identity is a real hazard. hautelook/templated-uri-bundle pointing at goetas/TemplatedUriBundle means you're on a fork whose provenance you should verify. Check the composer.json name, autoload PSR-4 prefix, and the actual bundle class. If the fork's namespace differs from the upstream docs you're reading, every snippet you copy will fail to autoload.
3.5.0 is a dependency major bump, not a feature release. The entire diff is "Update hautelook/templated-uri-router to 4.0" — no bundle code changed. Two consequences:
Pin explicitly (hautelook/templated-uri-bundle:3.5.*) and let composer.lock hold the router at a known-good 4.x, rather than floating on ^3.4.
Symfony compatibility is still the big risk — verify it, don't assume it. The bundle itself hasn't been touched since January 2023 and predates recent Symfony majors. The router bump may have improved things, but run composer why-not symfony/framework-bundle 7 (or your target version) and confirm both the bundle's and the router's composer.json constraints. With zero dependents, don't expect a community fix.
RFC 6570 syntax collides with Symfony's placeholder syntax. Symfony route paths use {name} for path variables. RFC 6570 also uses {name} but adds operators: {+path}, {#fragment}, {?query,params}, {/segments}. Expressions with ?, #, +, / inside braces can confuse the route compiler or get treated as literal characters in the compiled regex. Practical guidance:
{id} — those map cleanly.{?page,limit} to be matchable in a route path:; it's an output-only construct. Add query parameters to the template at generation time via the parameter array rather than baking the operator into the route definition.bin/console debug:router after adding anything exotic.Clearing the cache is not optional. The router (and any compiled route/template cache) won't notice route changes without bin/console cache:clear. If generateTemplated() returns stale or null output after a route edit — or immediately after the 3.5.0 upgrade — this is the first thing to try.
Encoding surprises. An expanded template's values get percent-encoded per the RFC's rules. A client expanding {id} with a/b may get a%2Fb, which then won't route the way you expect. Use reserved-expansion operators ({+id}) deliberately if you want slashes preserved — and understand the security implications of doing so. Re-check this specifically after the router 4.0 bump.
Templates are not clickable URLs. Never put the raw output of generateTemplated() into an href a browser will follow — the braces won't expand server-side. It belongs in a machine-readable payload (JSON-LD, HAL, OpenAPI), not in rendered HTML navigation.
Route leakage. Publishing templates exposes your route structure, including parameters you might not want advertised (internal IDs, admin flags). Gate template generation per-route rather than exposing everything the router knows.
Escape hatch: skip the bundle. It's a thin adapter over hautelook/templated-uri-router. If you only need expand($template, $vars) and not Symfony route-name integration, depend on the router library directly and delete this dependency. That removes the unmaintained-adapter risk — and for a package this size, it's usually the more defensible choice. Use the bundle only if the route-name-to-template mapping genuinely saves you work.
Extension points are limited. The practical ones are: decorate templated_uri.router with your own service implementing TemplatedRouterInterface (add caching, fallback, or logging there), or bypass the bundle. There's no plugin ecosystem to lean on.
How can I help you explore Laravel packages today?