vdb/uri
A Laravel/PHP package for building, parsing, and manipulating URIs with a fluent, immutable API. Compose paths and query strings, validate URL components, and work with clean URI value objects in your applications.
Install with Composer:
composer require vdb/uri
Open src/Uri.php first — it is the value object you will interact with 90% of the time. Then glance at the parser (UriParser or equivalent, depending on version) because it drives the static construction path. The README is thin; the class itself and the test suite are the real documentation for this package.
The first thing to get working is parsing a URL and reading back its RFC 3986 components:
use Vdb\Uri\Uri;
$uri = Uri::parse('https://[email protected]:8443/docs/page.html?sort=asc&page=2#top');
$uri->getScheme(); // "https"
$uri->getUserInfo(); // "alice"
$uri->getHost(); // "example.com"
$uri->getPort(); // 8443
$uri->getPath(); // "/docs/page.html"
$uri->getQuery(); // "sort=asc&page=2"
$uri->getFragment(); // "top"
(string) $uri; // round-trips the original string
The method names mirror java.net.URI, not PSR-7, so getUserInfo() / getHost() / getPort() are the accessors you reach for — not getAuthority() alone. Verify the exact surface against the version you install (Uri, UriReference, UriParser are the common class names in this family).
Wrap it, don’t scatter it. These classes are low-level. In application code, put them behind a small domain object (e.g. ExternalLink, CdnUrl, WebhookEndpoint) so you’re not passing Uri instances across layer boundaries and coupling your domain to a tiny, low-traffic dependency.
Resolving relative links against a base is the classic day-to-day use case — crawlers, sitemap builders, rewriting feeds:
$base = Uri::parse('https://example.com/blog/post-1/');
$abs = $base->resolve('../archive/2023?page=2');
echo $abs; // https://example.com/blog/archive/2023?page=2
resolve() follows RFC 3986 §5, so it correctly handles .., empty references, and query-only references. If you’re scraping or mirroring a site, this is the single most valuable method in the package.
Normalization for dedup and cache keys. When you need to compare “are these the same URL?”, normalize first, then compare the string forms — not the individual components:
$key = (string) Uri::parse($raw)->normalize();
$cache->remember($key, fn () => $client->get((string) $raw));
This handles case folding of scheme/host, dot-segment removal, and default-port stripping. It is safer than ad-hoc strtolower / rtrim logic.
Building URLs from parts for endpoint configuration:
$endpoint = Uri::parse('https://api.example.com/')
->withPath('/v2/webhooks')
->withQuery(http_build_query(['token' => $t]));
If the version you install exposes with* immutables, prefer them over string concatenation — you avoid the double-slash and missing-? bugs that plague hand-built URLs.
Bridging to PSR-7 / Guzzle. There is no built-in PSR-7 conversion. Do it at the boundary:
$psr = new \GuzzleHttp\Psr7\Uri((string) $vdbUri);
Parse with vdb/uri when you need RFC-correct resolution/normalization; hand off to your HTTP client as a string or PSR-7 UriInterface.
Testing. Use it in unit tests to assert that generated links are structurally what you expect:
$this->assertSame('/v2/items', Uri::parse($link)->getPath());
$this->assertSame('', Uri::parse($link)->getQuery()); // empty, not null
composer.json, and cover the behaviors you rely on with your own tests before an upgrade. If you need a long-term bet, league/uri is the mainstream alternative with the same RFC 3986 heritage.getScheme(), getHost(), getPath() and then assume it behaves like UriInterface. It doesn’t implement PSR-7, and with* mutability, __toString() failure behavior, and empty-vs-null returns may differ. Read the source before mixing it into anything that expects PSR-7.getQuery() and getFragment() return the raw component. RFC allows an empty query (?) and an empty fragment (#) that are distinct from “absent.” Don’t assume null — test for empty string explicitly if that distinction matters to you.%2F in a path segment, but you may still need rawurldecode() before storing values that originated as query parameters. Double-decoding is the common bug: decode once, at the edge, and keep Uri around only for structural operations.normalize() strips :80 / :443, but getPort() on the un-normalized object still reports what was written. If you compare ports directly you will get false negatives — compare normalized strings instead.https://a.com/x and https://a.com/x/ are different URIs and normalize() won’t merge them. If your cache or routing treats them as equivalent, do that collapsing in your own layer.idn_to_ascii() before parsing, or your comparisons will silently mismatch.Uri (if the constructor is protected and the parser is injectable in your version) or, more realistically, writing small decorators that consume the public getters. Assume no hooks and design around composition.(string) the intermediate pieces ($uri->getBaseUri(), then each component) rather than stepping through the parser — the string round-trip is the fastest signal for what the parser actually saw.How can I help you explore Laravel packages today?