Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Uri Laravel Package

sabre/uri

sabre/uri is a lightweight PHP library for working with URIs per RFC3986. It offers resolve, normalize, parse, build, and split functions for handling relative URLs, comparing URLs, and Windows-style paths. Fully unit tested, with many tests drawn from RFC3986 examples.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require sabre/uri

Version choice matters more than usual:

PHP Pin
8.2+ (Laravel 11/12) ^3.1
7.4 – 8.1 ^3.0
≤ 7.3 ^2.3

There is nothing to register. No service provider, no facade, no config file. Composer autoloads the functions via files, and you call them by namespace:

use function Sabre\Uri\parse;
use function Sabre\Uri\build;
use function Sabre\Uri\resolve;
use function Sabre\Uri\normalize;
use function Sabre\Uri\split;
use Sabre\Uri\InvalidUriException;

Look first at src/functions.php — the whole library is five functions plus one exception. Then skim RFC 3986 §5.2 if you care about why resolve() behaves the way it does.

First real use case: you scrape/consume an HTML page, feed, or API and get href="/docs/v2" or href="../images/a.png". PHP has no built-in for this.

$absolute = resolve('https://example.com/api/v1/items?page=2', '../images/a.png');
// https://example.com/api/images/a.png

That one call is usually the entire justification for the package. Everything else is a bonus.

Implementation Patterns

Resolving relative links in jobs / HTTP clients

This is the highest-frequency pattern in a Laravel app: pulling paginated data and following a Link: <...>; rel="next" header.

final class FollowLinks
{
    public function next(Illuminate\Http\Client\Response $response): ?string
    {
        $link = $response->header('Link');
        if ($link === '' || !preg_match('/<([^>]+)>\s*;\s*rel="?next"?/', $link, $m)) {
            return null;
        }

        return resolve($response->effectiveUri(), $m[1]);
    }
}

effectiveUri() is important — after redirects, the base must be the final URL, not the one you requested.

Canonical keys before caching or deduplicating

Two spellings of the same URL should not spawn two queued jobs or two cache entries.

public function canonical(string $url): string
{
    $parts = parse($url);

    // normalize() does NOT sort the query string or strip the fragment.
    unset($parts['fragment']);

    if (isset($parts['query'])) {
        parse_str($parts['query'], $q);
        ksort($q);
        $parts['query'] = http_build_query($q);
    }

    return normalize(build($parts));
}

Then Cache::lock('og:'.hash('xxh128', $canonical)) or a ShouldBeUnique job keyed on the canonical form.

A validation rule for user-supplied callback/webhook URLs

filter_var($url, FILTER_VALIDATE_URL) accepts things you don't want and rejects things you do. parse() gives you the components to make policy decisions yourself.

final class SafeCallbackUrl implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        try {
            $parts = parse((string) $value);
        } catch (InvalidUriException) {
            $fail("The {$attribute} is not a valid URI.");
            return;
        }

        if (($parts['scheme'] ?? null) !== 'https') {
            $fail("The {$attribute} must use https.");
        }

        if (isset($parts['user']) || isset($parts['pass'])) {
            $fail("The {$attribute} must not contain credentials.");
        }

        $host = strtolower((string) ($parts['host'] ?? ''));
        if (! in_array($host, config('services.callbacks.allowed_hosts', []), true)) {
            $fail("The {$attribute} host is not allowed.");
        }
    }
}

Guarding redirects (open-redirect prevention)

resolve() and normalize() make a relative Location absolute, but neither decides whether it's safe. That's your job:

function safe_redirect(string $location, Request $request): ?string
{
    try {
        $target = normalize(resolve($request->fullUrl(), $location));
        $targetParts = parse($target);
    } catch (InvalidUriException) {
        return null;
    }

    $own = parse((string) config('app.url'));

    if (($targetParts['host'] ?? '') !== ($own['host'] ?? '')
        || ($targetParts['scheme'] ?? '') !== ($own['scheme'] ?? '')) {
        return null;
    }

    return $target;
}

normalize() lowercases scheme and host, so the comparison is stable without extra strtolower() calls.

Building URLs from parts

Useful when a third-party gives you components rather than a string (S3 keys, signed URL reconstruction, mailto: links):

$mailto = build(['scheme' => 'mailto', 'path' => '[email protected]']);
// mailto:[email protected]

build() is the inverse of parse() and expects exactly that array shape: scheme, host, port, user, pass, path, query, fragment.

split() for storage paths

dirname()/basename() are locale- and separator-fragile. split() is a predictable last-slash split:

[$dir, $file] = split('https://cdn.example.com/uploads/2024/report.pdf');
// ['https://cdn.example.com/uploads/2024', 'report.pdf']

Asserting URLs in tests

Comparing URLs textually is a classic flaky test. Normalize both sides:

$this->assertSame(normalize($expected), normalize($actual));

Gotchas and Tips

resolve() has file-vs-directory semantics. This is the #1 bug:

resolve('https://example.com/api/v1', 'users');   // https://example.com/api/users  ← surprising
resolve('https://example.com/api/v1/', 'users');  // https://example.com/api/v1/users ← intended

If your base came from a config value or a route without a trailing slash, append one.

parse() throws; parse_url() returns false. Wrap every call in try/catch (InvalidUriException). Laravel's exception handler will happily 500 your API otherwise.

normalize() is not a canonicalizer for your purposes. It does not sort query parameters, does not strip fragments, and does not merge https://example.com with https://example.com/ beyond the RFC's empty-path rule. Do the extra work yourself (see the canonical-key pattern above).

build() performs zero percent-encoding. Whatever you put in path or query goes out verbatim. Run rawurlencode() / http_build_query() before handing values to it, or you'll ship URLs with raw & and spaces.

parse() percent-encodes non-ASCII before parsing. That's the fix for parse_url() corrupting Hebrew/Chinese input, but it means the path you get back is already percent-encoded. Don't rawurlencode() it a second time when round-tripping through build().

Windows paths changed behavior in 3.0.0. file:///C:/path/file.ext now yields an empty-string host and a leading-slash path. If you upgraded from 2.3.x and your file:// handling used host === null, re-test — this was a deliberate BC break (PR #82/#71, issue #81).

split() is a plain string split. Query strings and fragments stay glued to the basename ('a/b?x=1' → ['a', 'b?x=1']), and a trailing slash gives you an empty basename ('a/b/' → ['a/b', '']). Parse first if you need real components.

It will not protect you from hostile URLs. //evil.com is protocol-relative (host, no scheme), https://[email protected] has host = evil.com, and browsers treat \ like /. Always compare the parsed host against an allowlist after normalize(), never the raw string, and reject user/pass.

Function-based API is not mockable. You can't swap Sabre\Uri\resolve in a test. If you need fakes (or a seam for a future league/uri migration), wrap the five functions in a small injectable service class and type-hint that interface.

No PSR-7 interop. If you already use guzzlehttp/psr7, you have UriResolver available and may not need this. Conversely, converting parse() output into a UriInterface is trivial: new GuzzleHttp\Psr7\Uri(build($parts)).

Debugging: php artisan tinker → dump(Sabre\Uri\parse($url)) is faster than reasoning about regex. Print parse() on both the base and the relative reference before blaming resolve(); nine times out of ten the base is missing its trailing slash or carries a query string you didn't expect.

Performance: these are small pure-PHP functions, safe inside loops over thousands of feed entries. There's no caching layer, so if you canonicalize the same URL repeatedly in a request, memoize it in a local array.

Extension points are deliberately absent. No config, no events, no interfaces. When you need extra policy (IDN → punycode, default port handling, scheme allowlists), write it around the calls, in a Support\Uri helper or a dedicated service — not by forking the package.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource