rize/uri-template
RFC 6570 URI Template implementation for PHP. Expand templates into URLs and extract variables from matching URIs. Supports all expression types/levels, path segment and query expansions, plus base URI and default parameters—handy for building API endpoints.
Installation:
Add to composer.json:
"require": {
"rize/uri-template": "^0.4"
}
Run composer update.
Basic Expansion:
use Rize\UriTemplate;
$uri = new UriTemplate();
echo $uri->expand('/users/{id}', ['id' => 123]);
// Output: `/users/123`
First Use Case: Dynamically generate API endpoints in Laravel controllers:
$apiUrl = new UriTemplate('https://api.example.com/{version}');
$endpoint = $apiUrl->expand('/users/{id}', ['version' => 'v1', 'id' => 42]);
$client = new UriTemplate('https://api.twitter.com/{version}', ['version' => '1.1']);
$tweetUrl = $client->expand('/statuses/show/{id}.json', ['id' => 12345]);
// Output: `https://api.twitter.com/1.1/statuses/show/12345.json`
AppServiceProvider:
$this->app->singleton('api.uri', fn() => new UriTemplate(config('api.base_url'), config('api.defaults')));
Route::get('/users/{user:username}/posts/{post:slug}', function (UriTemplate $uri, $user, $post) {
$uri->expand('/users/{username}/posts/{slug}', ['username' => $user, 'slug' => $post]);
});
$serviceUrl = new UriTemplate('http://{service}.service.local/{resource}');
$url = $serviceUrl->expand('/users/{id}', ['service' => 'auth', 'id' => 1]);
$query = new UriTemplate();
$url = $query->expand('/search/{?q*,limit,offset}', [
'q' => ['php', 'laravel'],
'limit' => 10,
'offset' => 0
]);
// Output: `/search/?q=php&q=laravel&limit=10&offset=0`
% Modifier):
$url = $query->expand('/filter/{?filters%}', [
'filters' => ['user[role]' => 'admin', 'user[status]' => 'active']
]);
// Output: `/filter/?filters%5Buser%5D%5Brole%5D=admin&filters%5Buser%5D%5Bstatus%5D=active`
$template = '/users/{username}/posts/{post_id}';
$params = (new UriTemplate())->extract($template, request()->path());
// Extracts `username` and `post_id` from the URL.
$params = (new UriTemplate())->extract('/{?required*,optional}', request()->query(), true);
// Returns `null` if `required` params are missing.
Middleware for URI Validation:
public function handle(Request $request, Closure $next) {
$template = '/api/{version}/{resource}';
$params = (new UriTemplate())->extract($template, $request->path(), true);
if (!$params) abort(404);
return $next($request);
}
Form Request Validation:
public function rules() {
$uri = new UriTemplate();
$params = $uri->extract('/{?page,per_page}', $this->queryString);
return [
'page' => ['required_if' => $params['page'] ?? null, 'integer'],
'per_page' => ['required_if' => $params['per_page'] ?? null, 'integer'],
];
}
API Resource Serialization:
public function toArray($request) {
$uri = new UriTemplate();
$params = $uri->extract('/users/{user}', $request->path());
return [
'user_id' => $params['user'],
'url' => route('users.show', $params['user']),
];
}
Reuse Instances:
Instantiate UriTemplate once per request or service (e.g., in a service container) to avoid parsing overhead.
// In AppServiceProvider
$this->app->singleton(UriTemplate::class, fn() => new UriTemplate());
Cache Templates: For frequently used templates (e.g., API endpoints), cache the compiled patterns:
$cache = Cache::remember('uri_template_compiled', 60, fn() => new UriTemplate($template));
Strict Mode Misuse:
extract() returns null in strict mode if the URI doesn’t match the template exactly.!== null checks or provide fallback logic:
$params = (new UriTemplate())->extract($template, $uri, true);
if ($params === null) {
return response()->json(['error' => 'Invalid URI'], 400);
}
Query Parameter Encoding:
% modifier encodes [] as %5B%5D, which may not match client expectations.$decoded = str_replace('%5B', '[', str_replace('%5D', ']', $uri));
Default Parameters Override:
$uri = new UriTemplate('https://api.example.com/{version}', ['version' => 'v1']);
$url = $uri->expand('/{resource}', ['resource' => 'users', 'version' => 'v2']); // Overrides default
Nested Array Extraction:
% modifier may not handle deeply nested arrays as expected.$flatFilters = [];
foreach ($request->input('filters', []) as $key => $value) {
$flatFilters["filters[$key]"] = $value;
}
$params = (new UriTemplate())->extract('/{?filters%}', http_build_query($flatFilters));
PHP 8.1+ Requirement:
~0.3) if stuck on PHP 7.4.Validate Templates:
Use the extract method with strict => true to test templates against real URIs:
$params = (new UriTemplate())->extract('/{?page,per_page}', '/search?page=2', true);
// Debug: `var_dump($params)` to see extracted values.
Inspect Expansion: Log intermediate steps to debug complex templates:
$uri = new UriTemplate();
$parts = explode('/', $template);
foreach ($parts as $part) {
log("Processing part: $part");
}
Handle Edge Cases:
$url = $uri->expand('/{?tags*}', ['tags' => []]); // Output: `/`
$params = ['query' => 'hello world'];
$url = $uri->expand('/search/{?q}', $params); // Output: `/search/?q=hello+world`
// Hypothetical: Add a `:date` modifier to format timestamps.
$uri->expand('/events/{date:date}', ['date' => '202
How can I help you explore Laravel packages today?