6 min read

PHP 8.5's URI Extension: Modern URL Parsing Done Right

PHP 8.5's built-in URI extension replaces the broken parse_url() with RFC 3986 and WHATWG-compliant classes for immutable, safe URL handling.

Featured image for "PHP 8.5's URI Extension: Modern URL Parsing Done Right"

If you have been writing PHP long enough, you have a parse_url() war story. Maybe it was a URL with no scheme that silently returned a wrong structure, or a tricky edge case where authentication credentials in the URL caused an unexpected parse result, or the moment you realized that parse_url('//example.com/path') gives back false depending on context. The function has been part of PHP since version 4, and it has quietly caused bugs in production applications for over twenty years.

PHP 8.5, released in November 2025, ships a built-in URI extension that finally gives PHP developers a proper, standards-compliant alternative. It is always available — no pecl install, no composer require — and it handles URLs the way the rest of the modern web does.

Two Standards, Two Classes

The URI extension provides two distinct parsing classes corresponding to two different standards, and the distinction matters.

Uri\Rfc3986\Uri implements RFC 3986, the standard that defines how URIs are structured at the protocol level. This is the right choice for backend work: API endpoints, webhook URLs, redirect targets, internal service communication, and anywhere you need strict validation.

Uri\WhatWg\Url implements the WHATWG URL Standard, which is what browsers follow. If you are handling URLs submitted by end users or need to match browser behavior exactly (including edge-case normalization like resolving ../ path segments), this is the right class.

In practice, a single project might use both. An API client might validate outgoing request URLs with Uri\Rfc3986\Uri, while a web scraper normalizing user-submitted URLs reaches for Uri\WhatWg\Url.

Using Uri\Rfc3986\Uri

Construction is straightforward. Pass a string URI to the constructor and you get back an immutable object with named accessor methods:

use Uri\Rfc3986\Uri;

$uri = new Uri('https://api.example.com:8080/v2/users?page=2&sort=name#results');

echo $uri->getScheme();    // https
echo $uri->getHost();      // api.example.com
echo $uri->getPort();      // 8080
echo $uri->getPath();      // /v2/users
echo $uri->getQuery();     // page=2&sort=name
echo $uri->getFragment();  // results

Compare that to parse_url() which gives you an associative array with no type safety, no validation, and keys that may or may not be present depending on the input. The new extension raises an exception on invalid input, so your application can fail fast instead of silently processing a malformed URL.

Because the objects are immutable, modification returns a new instance — this is the “with-er” pattern that PHP’s readonly properties made popular:

use Uri\Rfc3986\Uri;

$base = new Uri('https://api.example.com/v2/users');

$paginated = $base
    ->withQuery('page=3&limit=50')
    ->withFragment('results');

echo $paginated->toString();
// https://api.example.com/v2/users?page=3&limit=50#results

// $base is unchanged
echo $base->toString();
// https://api.example.com/v2/users

This is a meaningful improvement over the old pattern of manipulating $_SERVER['REQUEST_URI'] strings by hand or concatenating URL components with sprintf.

Using Uri\WhatWg\Url

The WHATWG class shines when you need the browser-compatible path — normalization of relative segments, case folding on the scheme and host, and handling of the quirks that user-submitted URLs tend to have:

use Uri\WhatWg\Url;

// Browsers normalize this; parse_url() would not
$url = Url::fromString('https://Example.COM/path/../other?foo=bar');

echo $url->getHostname();  // example.com  (lowercased)
echo $url->getPathname();  // /other        (relative segment resolved)
echo $url->getSearch();    // ?foo=bar

The WHATWG class also handles the //host/path shorthand and javascript: or data: schemes without throwing, which makes it more forgiving for content processing use cases.

Practical Example: Building a Safe Redirect Helper

Here is a real-world scenario: validating that a user-supplied redirect URL stays within your own domain, a common source of open redirect vulnerabilities.

use Uri\Rfc3986\Uri;
use Uri\Rfc3986\InvalidUriException;

function isSafeRedirect(string $redirectUrl, string $allowedHost): bool
{
    try {
        $uri = new Uri($redirectUrl);
    } catch (InvalidUriException) {
        return false;
    }

    // Relative URLs (no host) are safe — they stay on-site
    if ($uri->getHost() === null || $uri->getHost() === '') {
        return true;
    }

    // Strip port for comparison
    $redirectHost = strtolower($uri->getHost());
    $allowed      = strtolower($allowedHost);

    return $redirectHost === $allowed
        || str_ends_with($redirectHost, '.' . $allowed);
}

// Usage
$allowed = 'myapp.com';

isSafeRedirect('/dashboard', $allowed);           // true
isSafeRedirect('https://myapp.com/home', $allowed); // true
isSafeRedirect('https://evil.com/phish', $allowed); // false
isSafeRedirect('https://notmyapp.com', $allowed);    // false

Previously, implementing this correctly meant juggling parse_url() output, handling the null/false return for malformed input, and remembering which array keys might be absent. The new classes make the intent explicit and the error handling clean.

Laravel Integration Notes

Laravel 12’s Illuminate\Http\Request and the URL generator still use PHP’s native URL primitives under the hood, so you will not see Uri\Rfc3986\Uri appearing as a first-class return type in Eloquent or the router just yet. That is expected — framework adoption of new PHP features takes a release cycle.

In the meantime, you can use the extension directly in service classes and form request validation:

use Uri\Rfc3986\Uri;
use Uri\Rfc3986\InvalidUriException;
use Illuminate\Validation\Rules\Rule;

// Custom validation rule
Rule::imageUrl(fn(string $value) => (function () use ($value): bool {
    try {
        $uri = new Uri($value);
        return in_array($uri->getScheme(), ['https'], true)
            && !empty($uri->getHost());
    } catch (InvalidUriException) {
        return false;
    }
})());

Symfony’s HttpFoundation team has also expressed interest in adopting the extension’s classes in future component releases, though as of PHP 8.5’s launch, no specific milestone has been announced publicly.

What About parse_url() Now?

It is not deprecated — it still works exactly as it always has. For simple cases where you know the input is well-formed and you just need one component, parse_url() is fine. But for anything that touches user input, external URLs from third-party APIs, or redirect logic, the URI extension is a strict upgrade.

The PHP Foundation’s announcement post on the extension describes it as an open source success story: the RFC was collaboratively developed with contributions from multiple PHP Foundation members and community reviewers, and the underlying parser libraries (uriparser for RFC 3986 and Lexbor for WHATWG) are mature, battle-tested C libraries. You are not getting a home-grown PHP implementation — you are getting a thin wrapper over production-proven parsing code.

Upgrading Your Codebase

If you are on PHP 8.5, the extension is already available. A practical approach to migration:

Find usages of parse_url() in your codebase with a grep or Rector rule, then categorize them by context. Backend API and redirect logic gets Uri\Rfc3986\Uri. User-facing URL input or HTML parsing gets Uri\WhatWg\Url. Anything where the current behavior has already been tested and poses no security concern can stay as-is and be migrated incrementally.

The extension is zero-dependency, zero-configuration, and fully backward-compatible — there is no reason not to start reaching for it in new code today.

Resources