Get "PHP 8 in a Nutshell" (Soon PHP 8.5)
Amit Merchant
Amit Merchant

A blog on PHP, JavaScript, and more

The new, standards‑compliant URI/URL API in PHP 8.5

PHP’s parse_url was not compliant with RFC 3986 (generic, permissive URIs) or WHATWG URL (browser-style URLs), leading to inconsistent behavior, interoperability problems with tools like cURL, and subtle security issues such as parsing confusion.

To fix these issues, PHP 8.5 introduces a standardized API so URIs/URLs are parsed, normalized, modified, and compared consistently with the relevant specs.


PHP now ships two immutable classes for parsing and working with addresses.

  • Uri\Rfc3986\Uri: follows RFC 3986 (generic URIs, strict validation, optional normalization, percent‑decoding where safe)

  • Uri\WhatWg\Url: follows WHATWG URL (browser behavior, IDNA/Unicode hosts, parse‑time transformations, soft/hard errors)

Both support parsing, resolving relative references, getting/setting components, comparing, and serializing.

What’s the difference between the two classes?

The classes differ in the specifications they implement and how they handle parsing, normalization, and error reporting.

  • RFC 3986 accepts URIs (may be scheme‑less), is strict on invalid characters, and offers “raw” vs “normalized‑decoded” views.

  • WHATWG only handles URLs (needs a scheme), auto‑normalizes on parse, keeps percent‑encoding in most components, and reports soft errors.

How these classes work?

These classes provide a clean, object-oriented way to work with URIs and URLs. You can create instances by parsing strings, access individual components, modify them immutably, and serialize back to strings.

Here’s how you can use them to parse and safely validate a URL.

use Uri\Rfc3986\Uri;
use Uri\WhatWg\Url;

$rfc = Uri::parse("https://example.com/path?x=1");   // Uri or null
$whatwg = Url::parse("https://example.com/path?x=1"); // Url or null

$bad = Uri::parse("invalid uri");                     // null (strict failure)
$errors = [];
$bad2 = Url::parse(" invalid url", null, $errors);    // null with soft/hard error details

WHATWG surfaces soft errors while still succeeding:

use Uri\WhatWg\Url;

$soft = [];
$url = new Url(" https://example.org", null, $soft);
echo $url->toAsciiString(); // https://example.org
// $soft[0]->type === UrlValidationErrorType::InvalidUrlUnit

Here’s how you can fetch URL components components (RFC raw vs normalized‑decoded).

use Uri\Rfc3986\Uri;

$u = new Uri("https://%61pple:p%61ss@ex%61mple.com:433/foob%61r?%61bc=%61bc#%61bc");

echo $u->getRawHost();     // ex%61mple.com
echo $u->getHost();        // example.com

echo $u->getRawPath();     // /foob%61r
echo $u->getPath();        // /foobar

echo $u->getRawUserInfo(); // %61pple:p%61ss
echo $u->getUserInfo();    // apple:pass

WHATWG returns a single normalized view for most components and keeps encoding:

use Uri\WhatWg\Url;

$w = new Url("HTTPS://%61pple:p%61ss@ex%61mple.com:433/foob%61r?%61bc=%61bc#%61bc");

echo $w->getScheme();      // https
echo $w->getUsername();    // %61pple
echo $w->getPath();        // /foob%61r
echo $w->getQuery();       // %61bc=%61bc

IDNA and Unicode hosts (WHATWG only)

use Uri\WhatWg\Url;

$w = new Url("https://🐘.com");
echo $w->getAsciiHost();   // xn--go8h.com
echo $w->getUnicodeHost(); // 🐘.com

Here’s how you can recompose the URL components back into a string.

RFC raw vs normalized:

use Uri\Rfc3986\Uri;

$u = new Uri("HTTPS://EXAMPLE.com");
echo $u->toRawString(); // HTTPS://EXAMPLE.com
echo $u->toString();    // https://example.com

WHATWG ASCII vs Unicode:

use Uri\WhatWg\Url;

$w = new Url("HTTPS://////你好你好.com");
echo $w->toAsciiString();   // https://xn--6qqa088eba.com/
echo $w->toUnicodeString(); // https://你好你好.com/

You can also check equality between two URIs/URLs.

By default, fragments are excluded.

use Uri\Rfc3986\Uri;
use Uri\WhatWg\Url;

$u1 = new Uri("https://example.COM#foo");
$u2 = new Uri("https://EXAMPLE.COM");
var_dump($u1->equals($u2)); // true

$w1 = new Url("https:////example.COM/");
$w2 = new Url("https://EXAMPLE.COM");
var_dump($w1->equals($w2)); // true

Include fragments when you need strict match:

use Uri\Rfc3986\Uri;

$u = new Uri("https://example.com#foo");
var_dump(
    $u->equals(
        new Uri("https://example.com"), 
        Uri\ComparisonMode::IncludeFragment
    )
); 
// false

A real-world example

Here’s a simple redirect-and-validate example using the new URI API.

Redirect HTTP to HTTPS and safely keep the path and query:

use Uri\Rfc3986\Uri;
use Uri\UriComparisonMode;

// Imagine this comes from an incoming request URL you need to validate and normalize
$incoming = "http://Example.COM/products/list?page=1&sort=price%2Bdesc";

// 1) Parse and validate strictly (RFC 3986)
$uri = Uri::parse($incoming);
if ($uri === null) {
    http_response_code(400);
    exit("Bad URL");
}

// 2) Normalize for consistent routing/logging (lowercase host, clean path)
$normalized = $uri->toString(); // https://example.com/... once we change scheme below
error_log("Normalized request: " . $normalized);

// 3) Enforce HTTPS by immutably changing just the scheme
$secure = $uri->withScheme("https");

// 4) Preserve path and query exactly as provided (safe for caches/signing)
$target = $secure->toString(); // normalized RFC 3986 string

// 5) Compare ignoring fragment (default) to avoid duplicate redirects
if (!$secure->equals($uri, UriComparisonMode::ExcludeFragment)) {
    header("Location: " . $target, true, 301);
    exit;
}

// Continue serving the HTTPS request...

In closing

Standards compliance fixes subtle bugs and security issues, like the parsing confusion exploited when FILTER_VALIDATE_URL disagrees with a client that follows RFC 3986.

The clear, immutable API reduce surprises; normalized views help routing/caching, while raw views preserve exact bytes for signing or proxying. WHATWG alignment matches browser behavior, including Unicode domains and automatic percent‑encoding.

You can read more about the new URI/URL API in the official RFC.

Coming Soon: PHP 8.5
Learn the fundamentals of PHP 8 (including 8.1, 8.2, 8.3, and 8.4), the latest version of PHP, and how to use it today with my book PHP 8 in a Nutshell. It's a no-fluff and easy-to-digest guide to the latest features and nitty-gritty details of PHP 8. So, if you're looking for a quick and easy way to PHP 8, this is the book for you.

👋 Hi there! This is Amit, again. I write articles about all things web development. If you enjoy my work (the articles, the open-source projects, my general demeanour... anything really), consider leaving a tip & supporting the site. Your support is incredibly appreciated!

Comments?