innmind/url-template
A PHP implementation of RFC 6570 URI Templates for expanding URL templates with variables. Build type-safe URLs using simple and complex expressions, with a clean API that fits the Innmind ecosystem and simplifies dynamic link generation.
composer require innmind/url-template
The package is a small, dependency-light RFC 6570 (URI Template) implementation that plugs into the rest of the Innmind ecosystem (innmind/url for the result object, innmind/immutable for the variable bag). It ships no Laravel service provider, config, or facade — you wire it yourself, which is usually five lines.
Read in this order:
README.md — the supported operators table ({var}, {+var}, {#var}, {?var}, {&var}, {.var}, {/var}, {;var}) and the list/explode modifiers.src/Template.php — the only entry point you normally need.src/ — this is where {?a,b} vs {?a*,b} behaviour lives, and it is the file you open when an expansion surprises you.First use case: building third-party API URLs where path segments and query parameters are optional, exactly like an OpenAPI servers/paths block.
use Innmind\UrlTemplate\Template;
use Innmind\Immutable\Map;
$template = Template::of('https://api.example.com/repos{/owner,repo}{?per_page,page}');
$url = $template->expand(Map::of(
'owner', 'Innmind',
'repo', 'UrlTemplate',
'per_page', '100',
));
echo $url->toString();
// https://api.example.com/repos/Innmind/UrlTemplate?per_page=100
Depending on the version you pin, Template::of() and/or expand() are Maybe-returning (older releases use Template::load(Str $template)). Check the signature once, then hide it behind your own factory so the rest of the codebase never sees Maybe.
Parse once, expand many. Parsing is the only non-trivial cost; expansion is cheap. Put templates in config and resolve them to Template instances in a container binding or a small registry, not inside the request path on every call.
// config/services.php
'github' => [
'repo' => 'https://api.github.com/repos{/owner,repo}{?per_page}',
],
final class Endpoint
{
/** @param Map<string, string> $variables */
public function __construct(
private Template $template,
private Map $variables,
) {}
public function url(): string
{
return $this->template->expand($this->variables)->toString();
}
}
Keep variable bags immutable and complete. Innmind's Map is the natural value type here; build it from a validated DTO or a form request rather than passing request()->all() straight in. Because Map::of() takes flat key/value pairs, a typed wrapper with a Map getter keeps call sites readable.
Use it as a URL builder, not a string builder. The winning integration is: template → expanded Url → your HTTP client (innmind/http, Guzzle, or Laravel's Http via (string) $url). Doing it this way means the same object is used for signing, logging, and retries, and you never hand-concatenate ? and &.
Partial expansion for pagination and re-templating. Expanding a subset of variables leaves the rest as literal {...} expressions in most implementations — useful for "render this template with the knowns, keep {?cursor} for the client". Verify this against your pinned version before relying on it; if it isn't supported, expand with the cursor omitted and treat the base URL as the reusable piece.
Data-driven tests. Port a handful of rows from the official uritemplate-test suite into a PHPUnit data provider. It is the fastest way to lock in operator behaviour and it catches regressions when you bump a major version — Innmind does release majors.
Cache-aware expansion. Expansion order follows the template, so identical inputs produce identical strings. That makes the output safe to use as a cache key component or to sign, as long as you never reorder variables in the template itself.
{var} vs {+var} is the classic footgun. Simple expansion percent-encodes reserved characters, so a value containing / or : becomes %2F. Use {+var} when the variable is itself a path or a full URL; use {var} for opaque identifiers. Getting this backwards produces 404s that look like routing bugs./.{?a} assumes no query string exists yet; {&a} appends. If your base template already carries a ?, starting a new expression with ? gives you a second ?. Reserve ? for the first expression and & for subsequent ones.* (explode) and :3 (prefix) change the shape of the value you must supply — lists and maps need Sequence/Map values, not strings. Start with scalars, and only reach for modifiers when a real API demands it.int from request()->integer() can blow up. Cast at the boundary ('per_page' => (string) $perPage).Map collapses duplicate keys and preserves insertion order; Sequence does not dedupe. When you build the variable bag from user input, duplicates are already gone by the time the template sees them — that's usually what you want, but don't rely on it for ordered list expansion.->unwrap() in request paths. Maybe/Attempt returns are there because a malformed template or bad expansion is a programming error, not a user error. Convert once at boot (fail fast) and pass the concrete Template/Url around.final; wrap rather than extend. Your own UrlBuilder/Endpoint type is the seam to hang logging, metrics, or required-variable validation on.%2520 style bugs.How can I help you explore Laravel packages today?