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 Router Laravel Package

ezsystems/templated-uri-router

A Symfony URL generator that outputs RFC 6570 templated URIs, letting you build links with placeholders like /demo{?page,sort*,filter*} from route parameters. It's a small, naive implementation that covers basic needs only.

View on GitHub
Deep Wiki
Context7

Getting Started

Read the README's usage block and stop there — that's the entire surface area. The package exposes one class, Hautelook\TemplatedUriRouter\Routing\Generator\Rfc6570Generator, whose constructor takes a Symfony RouteCollection and a RequestContext, and whose generate($name, $parameters) returns a string like /demo{?page,sort*,filter*}. That is the whole API.

Do not attempt to install this into a Laravel app. Two hard blockers:

  • It requires symfony/routing (2.x era) and type-hints Symfony\Component\Routing\RouteCollection / RequestContext. Laravel's Illuminate\Routing\RouteCollection is an unrelated class — there is no adapter, and the package is archived with zero dependents, so none will appear.
  • The composer name on Packagist (hautelook/templated-uri-router) does not match the repo (ezsystems/...); the ezsystems copy is a fork. The bundled Symfony integration lives in a separate, also-abandoned bundle repo.

Leverage the idea, not the artifact. RFC 6570 URI templates are genuinely useful for HATEOAS/JSON:API link blocks — you emit /demo{?page,sort*,filter*} and let the client fill in what it cares about. Implement that in Laravel with a maintained expander:

composer require rize/uri-template
use Rize\UriTemplate;

$template = (new UriTemplate)->expand('/demo{?page,sort*,filter*}', [
    'page'   => 2,
    'sort'   => ['name', 'created_at'],
    'filter' => ['active'],
]);
// /demo?page=2&sort=name&sort=created_at&filter=active

guzzlehttp/guzzle ships an equivalent (GuzzleHttp\UriTemplate\UriTemplate::expand) if you already depend on it.

First use case: a paginated index endpoint that returns templated links instead of hard-coded URLs, so a client can request ?page=3&sort=asc without the server knowing which combination it wants.

Implementation Patterns

Bind the expander as a singleton and wrap it behind an interface. This is the one place where copying the original package's design is worth it — keep the caller unaware of which library expands the template.

// app/Support/UriTemplates.php
namespace App\Support;

use Rize\UriTemplate;

class UriTemplates
{
    public function __construct(private UriTemplate $tpl = new UriTemplate) {}

    /** Build a template from a named Laravel route. */
    public function forRoute(string $name, array $queryVars = [], array $seeds = []): string
    {
        $route = \Route::getRoutes()->getByName($name);
        $path  = '/'.ltrim(explode('{', $route->uri())[0], '/');

        $vars = array_map(
            fn ($v) => is_array($v) ? $v.'*' : $v,  // * = explode for arrays
            $queryVars,
        );

        $template = $path.($vars ? '{?'.implode(',', $vars).'}' : '');

        return $seeds ? $this->tpl->expand($template, $seeds) : $template;
    }

    public function expand(string $template, array $vars): string
    {
        return $this->tpl->expand($template, $vars);
    }
}

Pattern 1 — hypermedia link block on collections. The direct analogue of the README example:

return response()->json([
    'data'  => $users,
    'links' => [
        'self'   => "/api/users?page={$users->currentPage()}",
        'next'   => $templates->forRoute('users.index', ['page', 'sort', 'filter'], ['page' => $users->currentPage() + 1]),
        'search' => $templates->forRoute('users.index'),
    ],
]);

The search link stays unexpanded — the client owns it.

Pattern 2 — route key / resource links. Give models a templatedLink() method that returns the canonical resource URI plus a templated self, so a single collection response supports both fixed and discoverable navigation.

Pattern 3 — API middleware that rewrites links. If you already emit self/next/prev via an ApiResource or JsonResource, a thin TemplatedLinksMiddleware (or a response()->macro) can convert the next entries into templates in one place instead of editing every controller.

Pattern 4 — Blade/@templatedRoute directive for server-rendered pages that hand a template to a JS client (e.g. an Alpine or htmx front-end that appends sort/filter params). Store the template in a data- attribute, expand client-side.

Pattern 5 — contract testing. Assert that a template expands to a URL your real route accepts. That's the only reliable guard against {?page} drifting from {page?}.

$this->get((new UriTemplate)->expand(
    route('users.index').'{?page}', ['page' => 2]
))->assertOk();

Gotchas and Tips

  • {?x} vs {&x} is the classic error. The first emits ?, the second emits & and is only legal when a query string already exists. Starting a template with {&page} produces &page=2 with a leading ampersand.
  • * (explode) changes array semantics. {?sort} with ['a','b'] yields sort=a,b; {?sort*} yields sort=a&sort=b. Laravel's $request->query('sort') reads the second form as an array and the first as a string — pick deliberately and parse consistently.
  • Undefined variables vanish. RFC 6570 drops null/unset variables entirely, so expand('/x{?page}', []) returns /x with no ?. That's usually what you want, but it means you cannot distinguish "no page requested" from "empty template" once expanded. Empty-string values are not dropped — they produce page=.
  • {+var} reserved expansion is what you need for path params containing slashes (e.g. a file path or an encoded ID). Simple {var} percent-encodes the slash and breaks route matching. Laravel's route parameter regex (where()) runs against the expanded URL, so a stray encoded slash triggers a 404 you'll spend an hour on.
  • Ordering is template order, not array order. The expander emits query pairs as written in the template. Don't rely on key order of the input array, and don't sign/cache URLs on the assumption it's stable — sort the template string itself.
  • Laravel routes use {param?}; RFC 6570 uses {?param}. They are not interchangeable and the derivation in forRoute() above is deliberately crude — it only handles trailing optional segments. For anything more complex, declare the template string explicitly next to the route definition and assert they agree in a test.
  • Don't emit templated URIs to browsers. A <a href="/demo{?page}"> is a dead link. They are for machine clients and for JS that calls expand(). Server-rendered navigation needs concrete URLs.
  • If you do port the original generator's calling convention — passing ['page' => '{page}'] as route parameters — be aware it bypasses Symfony/Laravel's requirement and URL-encoding rules. That hack is why the README says "extremely naive, and incomplete." Reimplement from scratch rather than translating its quirks.
  • Extension point worth keeping: the expander is a pure string function, so cache the compiled template (parsed variable list) per route name if you generate thousands of links per request. Route lookup via Route::getRoutes()->getByName() is the expensive part, not the expansion.
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.
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
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums