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

Templated Uri Bundle Laravel Package

ibexa/templated-uri-bundle

Symfony bundle (fork of hautelook/templated-uri-bundle) providing an RFC-6570 compatible templated URI router and URL generator. Exposes the hautelook.router.template service for generating templated route URLs.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the honest framing: you will not install this bundle in a Laravel app. ibexa/templated-uri-bundle is a Symfony bundle — its only source file is a HautelookTemplatedUriBundle class extending Symfony\Component\HttpKernel\Bundle\Bundle, registered via registerBundles() / config/bundles.php, exposing the hautelook.router.template service through a compiler pass that decorates Symfony's router. Laravel has no bundle kernel, no hautelook.router.template, and pulling the package in drags symfony/http-kernel, symfony/dependency-injection, and symfony/config into your tree where they will eventually fight Laravel's pinned symfony/console, symfony/routing, and symfony/http-foundation versions.

What you actually steal is the idea, not the package. The bundle exists to solve one problem: generate a concrete Symfony route and leave the query-string placeholders as literal {...} so an API client can expand them later (/demo?{&page}{&sort*}). That is ~30 lines of logic plus a well-specified standard (RFC 6570). Reimplement it in Laravel against Illuminate\Routing\UrlGenerator and you get the whole benefit with zero Symfony coupling.

Minimal steps:

  1. composer require guzzlehttp/uri-template (usually already present transitively via Guzzle 7) if you also need to expand templates — e.g. in tests, in an API gateway proxy, or in a console command that reads a HAL document.
  2. Add a tiny service provider binding a TemplatedUrl helper.
  3. Look first at the bundle's TemplatedRouterPass and its hautelook.router.template service definition — that is the entire extension point, and it tells you where to hook: the URL generator, not the router.
  4. First real use case: your GET /api/products index endpoint returning a links.filter / links.sort / links.page block of expandable templates instead of a dozen hand-built query-string variants.
// app/Support/TemplatedUrl.php
namespace App\Support;

use GuzzleHttp\UriTemplate\UriTemplate;
use Illuminate\Contracts\Routing\UrlGenerator;

class TemplatedUrl
{
    public function __construct(private UrlGenerator $url) {}

    /**
     * Build a template: the route stays concrete, the whitelisted
     * query params stay as {placeholders} for the client to fill in.
     */
    public function template(string $route, array $params = [], bool $absolute = true): string
    {
        $base = $this->url->route($route, [], $absolute);

        if ($params === []) {
            return $base;
        }

        // "?" starts the query, "&" appends; * = explode (repeatable) param
        $expressions = [];
        foreach (array_values($params) as $i => $name) {
            $operator = $i === 0 ? '?' : '&';
            $explode  = str_ends_with($name, '*') ? '*' : '';
            $expressions[] = $operator . rtrim($name, '*') . $explode;
        }

        return $base . '{' . implode(',', array_map(
            fn ($e) => ltrim($e, '?&') . (str_contains($e, '*') ? '*' : ''),
            $expressions
        )) . '}';
    }

    /** Expand a template you received (client side, tests, proxying). */
    public function expand(string $template, array $variables): string
    {
        return UriTemplate::expand($template, $variables);
    }
}
// app/Providers/TemplatedUrlServiceProvider.php
public function register(): void
{
    $this->app->singleton(TemplatedUrl::class);
    // mirror the bundle's router decoration: reach for the URL generator
    $this->app->alias(TemplatedUrl::class, 'templated-url');
}
$templated->template('api.products.index', ['category', 'page', 'sort*']);
// https://app.test/api/products{?category,page,sort*}

Implementation Patterns

Build in the resource, not in the controller. The Symfony bundle is naturally consumed inside a Hateoas/Bazinga serializer; the Laravel analogue is an JsonResource. Keep the template construction in the resource so every representation of a product carries the same expandable links.

public function toArray($request): array
{
    return [
        'id'    => $this->id,
        'name'  => $this->name,
        'links' => [
            'self'      => route('api.products.show', $this),
            'filter'    => app(TemplatedUrl::class)->template('api.products.index', ['q', 'category']),
            'sort_page' => app(TemplatedUrl::class)->template('api.products.index', ['sort', 'page']),
        ],
    ];
}

Whitelist operators; never interpolate raw input into the template string. The Symfony version takes an array and runs it through a UrlGenerator with a custom generator that re-encodes braces after encoding. In Laravel you are concatenating strings, so the array of param names is your security boundary. Validate names against ^[A-Za-z][A-Za-z0-9_]*(\*)?$ and reject anything with {, }, ?, &, # before it reaches the template.

Two directions, two helpers. Producing a template (template()) is string assembly. Consuming one (expand()) needs an RFC 6570 Level 4 implementation — use guzzlehttp/uri-template. Don't hand-roll expansion; the operator matrix ({var}, {+var}, {#var}, {.var}, {/var}, {;var}, {?var}, {&var}, plus * explode and :n prefix modifiers) is bigger than it looks.

Mirror the bundle's router-decoration trick with a URL generator macro if you want the Laravel ergonomics to feel native:

// AppServiceProvider::boot()
UrlGenerator::macro('templated', function (string $route, array $params = []) {
    return app(TemplatedUrl::class)->template($route, $params);
});

// url()->templated('api.products.index', ['page', 'sort*'])

This is the direct equivalent of $container->get('hautelook.router.template')->generate(...) — same call site shape, Laravel-flavoured.

Pair it with pagination. Laravel's paginator already emits concrete next_page_url/prev_page_url. Add a sibling page_template so clients can jump to an arbitrary page and keep their current filter set:

'page_template' => $templated->template('api.products.index', ['page']),
'filter_template' => $templated->template('api.products.index', ['q', 'category', 'sort*']),

Use it in docs and OpenAPI output. A console command that dumps every index endpoint's templates makes a decent zero-effort "discoverable API" artifact, and it exercises the helper without touching request paths — a safe pilot, exactly like the bundle's Symfony-side recommendation to start with non-critical routes.

Testing: expansion is pure, so test it without HTTP. Assert that expand($template, ['page' => 2, 'sort' => ['name', '-price']]) yields ...?page=2&sort=name&sort=-price, and assert that unset variables disappear rather than producing page=.

Gotchas and Tips

  • The bundle will not boot in Laravel. composer require may succeed (Symfony's HTTP kernel is installable), but the moment anything autoloads HautelookTemplatedUriBundle you get a missing-parent fatal, because Symfony\Component\HttpKernel\Bundle\Bundle is present but registerBundles() isn't a concept. Treat the package as documentation, not a dependency.
  • Version pinning matters if you do try it. The 6.0.0 release (Symfony 8, PHP service config replacing XML) is a different animal from 4.x/3.x. Laravel 11/12 ship Symfony 7 components; the 6.x line's symfony/* constraints will pull in a second wave of packages your symfony/console pin doesn't want.
  • {?a,b} vs {&a,b} is not cosmetic. RFC 6570's ? operator emits the leading ?; & assumes a query already started. The bundle's own README shows /demo?{&page} — a literal ? followed by &-operators — because the generator can't know whether the route already carried a query string. If you use {?a} on a base URL that already has ?foo=bar, expect ?foo=bar?a=1.
  • Existing query strings are a trap. url()->current() includes the query string; route('x') doesn't. Always derive the base with route() (no params) before appending a template, or you get duplicated ?.
  • Braces get percent-encoded the moment you pass them as values. UriTemplate::expand('{?q}', ['q' => '{page}']) gives ?q=%7Bpage%7D. The whole point of the Hautelook router is that braces in the template survive encoding — keep braces in the template literal, never in the variable array.
  • Blade double-escapes. {{ $template }} turns & into &; you need {!! !!} in HTML, or the client receives a broken URI. JSON/JsonResponse is safe.
  • {+var} is an open-redirect footgun. Reserved expansion keeps /, :, // intact — which is what you want for path segments and exactly what lets a crafted value escape your host. For path parameters use {/var} or validate against an enum.
  • Silent failure on missing variables. RFC 6570 says undefined variables expand to nothing; there's no exception. If a template is mandatory, assert non-empty expansions in tests or wrap expand() with a strict check.
  • * (explode) is the only sane way to express repeatable filters. {?sort*} expands ['name','-price'] to ?sort=name&sort=-price (or comma-joined depending on the operator family); {?sort} would give you sort=Array semantics via string coercion. Pick * deliberately.
  • Debug with the RFC's own examples. The spec's Appendix A/B tables are a ready-made fixture set; if your expansion diverges there, it will diverge in production. Note the bundle's changelog history of brace-encoding fixes and its acknowledged thin test coverage — the upstream library, not the Symfony wrapper, is where correctness lives.
  • Cost/benefit: 3 stars, 0 dependents, one maintainer fork. Any code you write here should depend on guzzlehttp/uri-template (battle-tested, Guzzle-maintained) plus ~50 lines of your own — not on the bundle. If you later migrate to Symfony, the bundle drops straight in because your call site is already generate()/expand()-shaped.
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.
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support