league/uri-polyfill
Polyfill for league/uri that supplies compatible URI parsing and manipulation classes, letting applications keep using the League URI API on environments where the full package or required extensions are unavailable. A drop-in compatibility layer for URL handling.
league/uri-polyfill backports the upcoming native PHP URI classes — Uri\Rfc3986\Uri and Uri\WhatWg\Url — to PHP 8.1+. It is a read-only split of league/uri, so the value is not in browsing this repo but in writing code today against the API you'll have natively tomorrow.
Minimal setup:
composer require league/uri-polyfill
There is nothing to register, no service provider, no config file. The classes are declared conditionally at autoload time (class_exists(..., false) guard), so on a PHP build that ships the native URI extension the polyfill silently steps aside and the real classes are used. That conditional declaration is the whole trick, and it means the package must be loaded through Composer's files autoload — which it is, by default.
First place to look: the upstream league/uri documentation and the PHP RFC for the URI extension (the "PHP native URI parsing" RFC). This repo is generated from that source; file issues and read docs upstream.
First use case — replacing parse_url():
use Uri\Rfc3986\Uri;
$uri = Uri::parse('https://user:[email protected]:8443/a/b?q=1#frag');
$uri->getScheme(); // "https"
$uri->getHost(); // "example.com"
$uri->getPort(); // 8443
$uri->getPath(); // "/a/b"
$uri->getQuery(); // "q=1"
$uri->getFragment(); // "frag"
(string) $uri; // round-tripped, normalized string
And for user-facing URLs, where you want browser-compatible behavior:
use Uri\WhatWg\Url;
$url = Url::parse('HTTP://Example.COM/foo bar'); // throws on malformed input
$url->getHost(); // "example.com" (lowercased, IDN → punycode)
(string) $url; // "http://example.com/foo%20bar"
Pick the right parser per input source. This is the single most important daily decision:
Uri\WhatWg\Url — anything a browser or user typed: form input, redirect targets, webhook URLs, sitemap entries. Implements the WHATWG URL spec: lowercases schemes/hosts, punycodes IDNs, percent-encodes spaces, adds a trailing / to bare origins, and rejects garbage.Uri\Rfc3986\Uri — protocol-level identifiers where you need strict RFC 3986 semantics and lossless round-tripping: DSNs, urn:/mailto:/tel: schemes, cache keys, internal resource identifiers.Wrap parsing in one place. Because the API is immutable and throws on failure, a tiny factory keeps callers clean and gives you a single seam to swap in the native classes later (they'll be the same classes, so this is mostly about error handling):
final class UrlFactory
{
public static function tryParse(string $value): ?Url
{
try {
return Url::parse($value);
} catch (\Uri\InvalidUriException) {
return null;
}
}
}
Build, don't concatenate. The with* methods return new instances and re-validate, which makes them ideal for query-parameter and path rewriting without the classic http_build_query + string-glue bugs:
$url = Url::parse($request->fullUrl())
->withQuery(\Arr::query(['page' => 2] + $request->query()))
->withFragment(null);
Relative resolution instead of manual URL math. Both classes expose a resolve-style operation against a base URI. Use it for crawling, canonical-link rewriting, and rewriting assets in email templates:
$absolute = Url::parse($base)->resolve($relative);
Dedupe with equals()/normalized output, not ===. Parse both sides with the same parser, then compare. Url compares serialized forms (with an option to ignore the fragment); raw string comparison will not catch HTTPS://Example.com vs https://example.com/.
Laravel integration points. Keep it thin — this is a value object library, not a framework package:
Rule object wrapping Url::parse() in a try/catch, so validation and parsing agree on what "valid" means.AsUrl value object stored as a string column, serializing via (string) $uri.$request->fullUrl(), compare host against config('app.url'), redirect.UriServiceProvider-free binding in a small Uri facade or helper if you want uri('...') ergonomics.Adapter boundary for PSR-7 / Guzzle. There is no interop layer here. Convert explicitly at the edges: (string) $uri into new GuzzleHttp\Psr7\Uri(...) and Uri\Rfc3986\Uri::parse((string) $psrUri) coming back. Keeping the conversion in two named helpers stops the two object models from bleeding into your domain code.
Do not assume the polyfill is the code path. If the native extension is present, Uri\Rfc3986\Uri is the C implementation, not this package. Behavioral drift between polyfill and native is the realistic production risk; pin a CI matrix that runs at least one job with the extension enabled.
Uri\... ≠ League\Uri\.... league/uri ships League\Uri\Uri, League\Uri\Http, etc., with a different (non-native) surface. Mixing use statements from both namespaces in one file is a common and confusing mistake — the class names look nearly identical but the methods and exception types don't line up.
Exceptions live in the Uri namespace. Catch \Uri\InvalidUriException (and the WHATWG-specific subclass) rather than \InvalidArgumentException. Uncaught parse exceptions on user input are the #1 way this bites you — always parse untrusted strings inside a try.
Autoload ordering matters. The classes are registered via Composer's files autoload. If you have a custom bootstrap, a hand-rolled autoloader, or you require a file before vendor/autoload.php, you'll get "class not found". composer dump-autoload --classmap-authoritative is fine; a classmap-only setup that skips files is not.
Preloading is safe but verify it. opcache.preload with the polyfill guarded by class_exists(..., false) won't fatal, but preloading a stale copy is a classic "works locally, 500s in prod" cause. Skip preloading these files unless you've measured a benefit.
Normalization is not idempotent across parsers. Uri\WhatWg\Url will happily turn http://example.com into http://example.com/ (added slash) and lowercase everything; Uri\Rfc3986\Uri preserves what you gave it. Store the parser choice next to the data you persist — a URL normalized by WHATWG and later re-parsed by RFC 3986 is a subtle source of duplicate rows.
toString() vs toRawString() (and Unicode variants). The normalized/pretty form is not always the byte-exact form. If you're signing requests, hashing URLs for cache keys, or round-tripping through a third party, use the raw/serialized form and confirm which method you're on. For Url, the WHATWG class additionally exposes a Unicode/display form for showing IDN hosts to humans — never persist or compare that one.
Default ports are dropped. getPort() returns null for https://example.com:443/. Don't write if ($uri->getPort() === 443) — compare getPort() ?? $defaultForScheme.
getUserInfo() shape differs from parse_url(). RFC 3986 returns user and password as a tuple rather than parse_url's colon-joined user string. Credentials may also be percent-encoded; decode deliberately, and never log the result.
Report bugs upstream. This repo is READ-ONLY and marked as a split ([READ-ONLY]). Issues, PRs, and security reports belong in thephpleague/uri. With ~0 dependents it gets no independent maintenance attention — treat it as a generated artifact and pin a version.
Version churn is a feature here. The last release is dated in the future relative to typical assumptions, because the split tracks the RFC as it lands in PHP core. Expect the API to be frozen to the native signature, not to league/uri's. If you need stability today, wrap the few methods you actually use behind your own interface — that's also the cheapest insurance if the polyfill is ever dropped once the minimum PHP version ships the extension natively.
How can I help you explore Laravel packages today?