Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Uri Template Laravel Package

guzzlehttp/uri-template

Expands RFC 6570 URI templates into concrete URI strings in PHP, turning templates like /users/{id}{?tab} into real paths with query strings from an array of values.

View on GitHub
Deep Wiki
Context7

Getting Started

Check whether it is already there. In most Laravel apps guzzlehttp/uri-template arrives transitively via guzzlehttp/guzzle. Before adding a constraint, run:

composer show guzzlehttp/uri-template
composer why guzzlehttp/uri-template

If it is present, you can use GuzzleHttp\UriTemplate\UriTemplate; immediately — no service provider, config file, or publish step exists. Only add it explicitly (composer require guzzlehttp/uri-template) when you want to pin the major and guarantee it survives a Guzzle upgrade.

Where to look first. The README quick start is the whole public API: UriTemplate::expand(string $template, array $variables): string. Then read docs/uri-template-usage.md for the operator matrix and docs/input-contract.md for exactly what value shapes are accepted (scalars, dense lists, associative maps, nested arrays for exploded query params). If you are upgrading from ^1.0, read UPGRADING.md — v2 changed behaviour, not just signatures.

First use case. The canonical Laravel job is expanding a template that a third party handed you, typically a hypermedia link or an API-discovered endpoint:

use GuzzleHttp\UriTemplate\UriTemplate;

// API returned: {"search_url": "https://api.example.com/v2/search{?q,page,per_page}"}
$url = UriTemplate::expand($response->json('search_url'), [
    'q'        => 'invoice',
    'page'     => 2,
    'per_page' => 50,
]);

// https://api.example.com/v2/search?q=invoice&page=2&per_page=50
Http::get($url)->json();

That is the mental model to hold: this package is a string formatter, not an HTTP client, not a router. route() and url() know nothing about it and vice versa.

Implementation Patterns

Wrap it once, then forget it. The static call is untestable to mock and easy to sprinkle everywhere. A two-line wrapper keeps call sites greppable and gives you one place to add logging or input sanitising later:

namespace App\Support;

use GuzzleHttp\UriTemplate\UriTemplate;

class UriExpander
{
    public function expand(string $template, array $variables = []): string
    {
        return UriTemplate::expand($template, $variables);
    }
}

Bind it as a singleton in AppServiceProvider only if you plan to decorate it (e.g. add metrics); otherwise constructor-inject the concrete class and let the container autowire it.

Config-driven endpoint templates. When an external service documents its endpoints as templates, store the template verbatim in config/services.php rather than pre-baking URLs. It keeps the upstream contract visible and lets you vary variables per call:

// config/services.php
'warehouse' => [
    'base'    => env('WAREHOUSE_URL'),
    'item'    => '/api/v1/items/{sku}{?expand,fields}',
    'history' => '/api/v1/items/{sku}/history{/year}{?cursor}',
],
$url = config('services.warehouse.base')
     . $this->expander->expand(config('services.warehouse.item'), [
         'sku'    => $sku,
         'expand' => ['stock', 'pricing'],   // dense list -> explode
         'fields' => ['a' => 1, 'b' => 2],    // assoc -> map
     ]);

Hypermedia / pagination crawling. This is where the package earns its keep in a Laravel job or console command. APIs that advertise next/prev links as templates need cursor values merged in at each hop:

$template = $response->json('meta.pagination.next');  // "{?cursor,limit}"
$cursor   = null;

do {
    $url = UriTemplate::expand($template, array_filter([
        'cursor' => $cursor,
        'limit'  => 100,
    ], fn ($v) => $v !== null));

    $page   = Http::get($url)->json();
    $cursor = $page['meta']['pagination']['next_cursor'] ?? null;
} while ($cursor);

array_filter with a null-preserving callback is deliberate — see the gotchas on sparse arrays.

Multi-tenant callback / webhook URLs. Tenants often provide a URL pattern with placeholders rather than a fixed URL. Store the pattern, expand per event:

$url = $this->expander->expand($tenant->webhook_template, [
    'tenant' => $tenant->slug,
    'event'  => $event->type,
    'id'     => $event->ulid,
]);
// "https://{tenant}.hooks.example.com/{event}{/id}"

Bridging Laravel request data into a template. You almost never pass $request->all() straight through — validate first, then hand over only the variables the template names. Since there is no public introspection API, a small regex extractor is the pragmatic extension point:

preg_match_all('/\{[+#.\/;?&]?([^}]+)\}/', $template, $m);

$names = collect(explode(',', implode(',', $m[1])))
    ->map(fn ($n) => ltrim(trim(explode(':', $n)[0]), '+#'))
    ->filter()
    ->all();

$vars = $request->only($names);   // or $request->validated()

Cache that extraction (Cache::remember keyed by md5($template)) if you expand in a hot path.

Testing. Expansion is pure and deterministic — assert exact strings, no mocks. A data provider over the RFC 6570 operator matrix is far more valuable than testing your wrapper:

public static function templates(): array
{
    return [
        'simple'    => ['/users/{id}',        ['id' => 42],   '/users/42'],
        'reserved'  => ['/files/{+path}',    ['path' => 'a/b'], '/files/a/b'],
        'query'     => ['/s{?q,page}',        ['q' => 'x'],   '/s?q=x'],
        'undefined' => ['/s{?q}',             [],             '/s'],
        'list'      => ['/s{?tags*}',        ['tags' => ['a','b']], '/s?tags=a&tags=b'],
    ];
}

Use Http::fake() for the surrounding request, and assert the faked URL matches the expansion.

Gotchas and Tips

v1 and v2 are not interchangeable. UriTemplate is non-instantiable in v2 — new UriTemplate() was the v1 style and now fails. Also in v2, invalid template syntax throws instead of expanding partially, which means templates that silently "worked" for years can start throwing after composer update. Run your test suite with --prefer-lowest at least once after bumping.

Sparse arrays become maps, silently. Dense zero-indexed arrays expand as lists (tags=a&tags=b); anything with gaps or string keys expands as a map (tags[0]=a). The classic bug is array_filter($request->input('tags')) — it preserves keys, so your list turns into a map and the upstream API rejects it. Always array_values() after filtering.

null means "undefined", and undefined variables vanish entirely. {?foo} with ['foo' => null] produces no ? at all. But ['foo' => ''] is defined in v2 and produces ?foo=. If an empty string should mean "not provided", normalise it yourself before expanding.

Booleans expand as 1 and 0 — at every nesting level, in both majors. There is no true/false output. If an API expects ?active=true, pass the string, not the bool.

User input can trigger exceptions. v2 validates UTF-8 and rejects invalid byte sequences, non-finite floats (INF, NAN), prefix modifiers ({var:3}) applied to lists/maps, and __toString() failures — all with InvalidArgumentException. Anything sourced from request() or a DB column with mixed encodings is a candidate. Wrap expansions that touch external data:

try {
    $url = $this->expander->expand($template, $vars);
} catch (\InvalidArgumentException $e) {
    report($e);           // message includes the member path, e.g. tags[2]
    throw new \RuntimeException('Bad URL variables', previous: $e);
}

Don't put $vars into the log blindly — they may contain tokens.

{var} vs {+var} is the encoding decision that bites. {var} percent-encodes reserved characters, so a/b becomes a%2Fb — correct for a single path segment. {+var} (reserved expansion) preserves /, :, ? and existing percent-encoded triplets — correct for multi-segment paths you have already built. Choosing wrong gives you either 404s or double-encoded %252F.

Only one place should build the query string. If you expand a {?...} template, do not also pass Http::withQueryParameters([...]) or append ?$query — you will get a duplicated or re-encoded query string, and Laravel's HTTP client will happily accept the mess. Expand once, pass the finished string to Http::get().

Composer version pinning is a real constraint. Newer Guzzle majors require the newer template major. If you explicitly require ^2.0 while an installed guzzlehttp/guzzle requires ^1.0, Composer cannot satisfy both for the same package name and will hard-fail. Decide which Guzzle you are on first; don't add the direct dependency "just to be safe".

Don't serialise it. v2 explicitly rejects native serialize()/unserialize(). The class is stateless anyway — never put it on a queued job or in the session; just call it inside the job. Same reason there is nothing to cache or pre-warm: expansion is microseconds, so caching is only worth it for the template-parsing regex, not the expander.

No container binding, facade, or config ships with it — and you should not invent one for a two-line static API. Resist wrapping it in a UriTemplate facade; it adds indirection without testability, since the underlying call is already pure.

Trust boundary. Expanding a template supplied by a remote service using your variable bag is a data-exfiltration risk if the template can reference arbitrary variable names — the package will happily interpolate anything you hand it. When templates are untrusted, pass an explicit allow-listed array, never the full request or a merged options array.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator