spomky-labs/uri-util
A lightweight PHP utility for parsing, normalizing, and manipulating URIs and URI references according to RFC 3986. It helps resolve relative paths, inspect components, and build reliable URL handling in frameworks and libraries without heavy dependencies.
This is a bare PHP library — no service provider, no facade, no config file. Install it and read the source; the surface area is small enough to audit in one sitting.
composer require spomky-labs/uri-util
Then look first at:
vendor/spomky-labs/uri-util/composer.json — check the PHP constraint and whether it pulls psr/http-message or guzzlehttp/psr7. This determines whether you can hand its objects to Guzzle/Http::withOptions.vendor/spomky-labs/uri-util/src/ — the README is thin; the classes (Uri, Path, and query-string helpers) are the documentation. Note the exact namespace (SpomkyLabs\UriUtil\…) and whether methods are static helpers or immutable value objects.First use case: canonicalize external URLs before you store, compare, or sign them. A typical Laravel entry point is a redirect/callback controller that receives a user-supplied redirect_uri or a signed storage URL and needs a stable, comparable form:
use SpomkyLabs\UriUtil\Uri;
$normalized = (string) Uri::fromString($request->query('redirect_uri'))
->withScheme(strtolower(...)) // adapt to actual API
->withFragment('');
Start with one narrow job — normalize a URL, or join path segments — and put it behind your own service class so you can swap it later.
Canonical redirects in middleware. Normalize before deciding whether to redirect. Strip default ports, lowercase the host, collapse dot segments, drop the fragment. Return 301 only when the normalized form differs from the request URI.
Signing and webhook verification. Both sides must canonicalize identically. Build the string-to-sign from the normalized URI (path + sorted query, encoded exactly once, no fragment) so a client using ?a=1&b=2 and a server logging ?b=2&a=1 agree.
class CanonicalUri
{
public function __construct(private string $raw) {}
public function __toString(): string
{
$uri = Uri::fromString($this->raw);
// strip fragment, normalize path, re-encode query
return (string) $uri;
}
}
Building URLs from config. Use the path helpers to join segments instead of rtrim($base, '/').'/'.ltrim($path, '/'):
$endpoint = Path::join(config('services.api.base'), 'v1', 'users', (string) $id);
This avoids double slashes and handles empty segments more predictably than string concatenation.
Deduplication and cache keys. Hash the normalized URI rather than the raw string. Two rows pointing at the same resource with different casing or trailing slashes collapse to one key.
Same-origin checks. Parse both URIs and compare scheme + host + port. Do not string-compare; https://example.com and https://example.com:443 are the same origin.
Adapter for future migration. Wrap every call in one small class (App\Support\UriNormalizer) with normalize(string): string and join(string ...$segments): string. When you move to league/uri, only that class changes.
Don't use it for route generation. Laravel's route() and url() already produce app URLs correctly. Reach for this package only for opaque/external URLs that Laravel doesn't own.
Testing pattern. Feed the RFC 3986 reference examples through both your code and the expected normalized output in a data provider; this catches encoding regressions fast.
withPath()/withQuery() return new instances (likely, given the PSR-7 influence), $uri->withPath('/x'); silently does nothing. Always reassign: $uri = $uri->withPath('/x');.%2F in path segments being decoded into a real slash, spaces becoming + in queries but %20 in paths, and double-encoding when you pass an already-encoded string back in. Normalize once, at the boundary.xn-- form. Do that explicitly if you accept international domains.#… before HMACs and before persisting as a redirect target unless you truly need it.a=1&a=2), bracket arrays (a[b]=c), and valueless keys (?flag) are where hand-rolled parsers break. Don't assume parse_str semantics match.../foo against a base URL is not a core feature; implement it yourself or move to league/uri.Psr\Http\Message\UriInterface, convert via (string) and GuzzleHttp\Psr7\UriResolver before handing it to Guzzle or Laravel's HTTP client.composer.json, keep the adapter boundary, and plan an exit to league/uri for anything security-adjacent (signing, redirect validation).bin2hex((string) $uri) for both sides — invisible percent-encoding or trailing-slash differences show up immediately.final or change without notice.How can I help you explore Laravel packages today?