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

ezsystems/templated-uri-bundle

Symfony bundle for the Hautelook TemplatedUriRouter library, adding an RFC-6570-compliant router and URL generator. It exposes the hautelook.router.template service so you can generate templated links with expressions such as {page} and {&sort*}. Install via Composer and register the bundle in y...

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check that determines your whole day: this is a Symfony bundle, not a Laravel package. composer require hautelook/templated-uri-bundle into a Laravel app gives you a HautelookTemplatedUriBundle class and a hautelook.router.template service ID that Laravel's container knows nothing about, and AppKernel.php / bundles.php registration steps that don't exist in your app. You cannot "just install it" and call app('hautelook.router.template').

What you actually want is the two-line idea at the heart of the README:

$router->generate('hautelook_demo_route', [
    'page'   => '{page}',
    'sort'   => ['{sort}'],
    'filter' => ['{filter}'],
]);
// => /demo?{&page}{&sort*}{&filter*}

Given a named route and a parameter map where values are placeholders instead of literals, it emits an RFC-6570 URI template rather than a concrete URL. That is the capability; the bundle is a thin Symfony wrapper around it.

So your path in Laravel:

  1. Read the README usage block and the hautelook/TemplatedUriRouter library — that's where the actual generation logic lives. The bundle adds service registration and nothing else worth porting.
  2. Install a maintained RFC-6570 expander instead of the bundle: composer require guzzlehttp/uri-template (or rize/uri-template). Both do RFC-6570 expansion and are actively maintained. You don't need the routing-generation half — Laravel's route() already gives you the path.
  3. First use case: a HAL-style _links block on a paginated collection resource, where the client needs to discover ?page=, ?sort= and ?filter= without you hard-coding those URLs.

Minimum viable first slice:

// app/Services/UriTemplates.php
use GuzzleHttp\UriTemplate\UriTemplate;

class UriTemplates
{
    public function forRoute(string $name, array $templateVars, array $routeParams = []): string
    {
        $base = route($name, $routeParams); // concrete path, no query string
        $query = [];

        foreach ($templateVars as $key => $spec) {
            $query[] = $spec['explode']
                ? '{&' . $key . '*}'
                : '{&' . $key . '}';
        }

        return $base . implode('', $query);
    }
}
// in a JsonResource
' _links' => [
    'self'  => route('orders.index'),
    'next'  => app(UriTemplates::class)->forRoute('orders.index', [
        'page' => ['explode' => false],
        'sort' => ['explode' => true],
    ]),
],
// => https://api.example.test/orders{&page}{&sort*}

You already ship the second half of the contract for free: UriTemplate::expand($template, ['page' => 2, 'sort' => ['created_at', 'id']]) on the client side, or any JS uri-template library.

Implementation Patterns

Interface + provider binding. Don't let the concept leak. Bind a small interface so you can swap implementations (Guzzle today, in-house tomorrow) without touching resources:

interface TemplatedUriGenerator
{
    public function generate(string $routeName, array $templateVars, array $routeParams = []): string;
}
// AppServiceProvider::register
$this->app->singleton(TemplatedUriGenerator::class, GuzzleTemplatedUriGenerator::class);

Keep route generation and template generation separate. This is the single most important workflow decision. route() resolves path segments and required params; your template builder only ever appends query-continuation expressions ({&var}, {&var*}). Trying to template path segments through Laravel's router (e.g. orders/{orderId}) is fighting the framework — emit the concrete path and template only the query surface unless you have a hard requirement for {/id} style path expansion.

Optional vs. required parameters. RFC-6570 gives you the semantics for free:

  • {&page} — emitted only if the client supplies page.
  • {&sort*} — explode modifier; a list value becomes &sort=a&sort=b. Use for multi-value filters/sorts/sparse fieldsets.
  • {?page,sort} — the ? operator gives you the leading ? and , joining in one expression, useful when you want one expression instead of chained {&...}.

Prefer one {?...} expression for readability in payloads; the chained {&...} form is fine and slightly easier to build incrementally.

Resource-transformer integration. The natural Laravel home is inside JsonResource::toArray(), delegating to a dedicated link-builder so resources don't assemble template strings:

class OrderResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'     => $this->id,
            '_links' => [
                'self'   => route('orders.show', $this->id),
                'cancel' => route('orders.cancel', $this->id),
                'index'  => app(TemplatedUriGenerator::class)->generate('orders.index', [
                    'filter' => ['explode' => true],
                    'sort'   => ['explode' => true],
                    'page'   => ['explode' => false],
                ]),
            ],
        ];
    }
}

A fluent link builder pays off fast once more than two resources need templates — ->link('orders.index')->with('sort', explode: true)->with('page'). It also becomes the one place you enforce "no duplicate & operators" and "no empty expressions".

Version/contract tests. Write expansion contract tests, not string-snapshot tests. For each route you template, assert:

$this->assertSame(
    'https://api.test/orders?page=2&sort=created_at&sort=id',
    UriTemplate::expand($template, ['page' => 2, 'sort' => ['created_at', 'id']])
);
$this->assertSame('https://api.test/orders', UriTemplate::expand($template, []));

That second assertion (empty variables ⇒ clean base URL) is the one that catches 90% of template bugs, including the dangling ? case.

Documentation and OpenAPI. Store the template strings next to your route definitions or in a config map (config/uri_templates.php) so /docs can list "expandable parameters per endpoint" without reflection over controllers.

Gotchas and Tips

The package is archived and effectively abandoned. 1 star, 0 dependents, README/CHANGELOG-only maturity, no verified PHP 8.x or modern Symfony support. Do not put it in your composer.json — you would own it forever. Treat the repo as a design reference and take a maintained RFC-6570 library.

Name/provenance mismatch. The repository is ezsystems/templated-uri-bundle but the README and Composer name are hautelook/templated-uri-bundle, and the docs still say "Hautelook Templated URI Bundle." Before you trust any code you find, confirm which upstream it actually mirrors. If you ever do need it in a Symfony app, pin the canonical hautelook/* name and check the tag history (v0.1 → 3.0.0).

route() will percent-encode your braces. route('orders.index', ['page' => '{page}']) produces ...?page=%7Bpage%7D — useless as a template. This is exactly the bug the upstream bundle exists to work around ("Fixed braces that should not be encoded" in the 1.0.0 changelog). In Laravel: generate the base URL with no template params, then append the raw expression string. Never round-trip a template through urlencode or through route()'s parameter array.

Placeholders are not validated. The router treats any string as a literal unless it looks like a template expression; there's no check that {page} is declared in your API contract. A typo like {pgae} ships silently and only breaks clients. Validate template variable names against a whitelist in your builder.

Explode (*) only makes sense on query/continuation expressions. {&sort*} is fine; {sort*} in a path segment or a {#frag} context will produce output clients mis-expand. Restrict explode: true to &/? operators.

Clients must implement expansion. A template in your payload is inert until the consumer runs RFC-6570 expansion. Document the required client library (uri-template for JS, guzzlehttp/uri-template for PHP SDKs) or your _links.next will be pasted literally into HTTP clients and 404.

Caching. Templates are deterministic — cache them per route name in the container or a config cache. Do not regenerate per resource instance inside a 200-item collection response.

No extension points to lean on. You can't subclass your way out of a stale Symfony bundle in a Laravel app. Your extension point is the interface you defined; keep the Guzzle dependency behind it so a future swap is a one-line binding change.

Debugging tip. When a template misbehaves in a payload, expand it immediately with a couple of representative variable sets before blaming the client. Mismatched results almost always mean an unencoded brace, a missing & operator (producing ?page=2sort=a), or two adjacent expressions that should have been one.

Do the build-vs-buy math honestly. The adapter is ~50–100 lines. The archived dependency is a permanent governance line item. For a Laravel codebase the answer is nearly always: wrap a maintained expander, own the 80 lines, move on.

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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource