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

Purl Laravel Package

jwage/purl

Purl is a lightweight, dependency-free PHP library for parsing and manipulating URLs. It provides a fluent object-oriented wrapper around parse_url, making it easy to inspect and modify schemes, hosts, paths, query strings, and fragments in your applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Purl is a plain PHP library — there is no service provider, config file, or facade to publish. Install it and you are done:

composer require jwage/purl

The only entry point you need is Purl\Url. Read that class first; Purl\Path and Purl\Query are the two sub-objects it delegates to for path and query-string work. The static factory is the cleanest way in:

use Purl\Url;

$url = Url::parse('http://api.example.com/v1/users?page=2');
$url->set('scheme', 'https');
$url->query->set('per_page', '50');
$url->path->set('/v2/users');

echo (string) $url;
// https://api.example.com/v2/users?page=2&per_page=50

A realistic first use case in a Laravel app: you have a third-party base URL in config/services.php and need to build an outbound request URL by swapping the scheme, appending an API key, and keeping whatever query parameters were already configured. Purl lets you do that in three lines instead of string concatenation and parse_str/http_build_query gymnastics.

Implementation Patterns

Wrap it in a tiny value-object helper. Rather than sprinkling Url::parse() calls across controllers, put the manipulation in one place:

namespace App\Support;

use Purl\Url;

class Endpoint
{
    public static function for(string $base, array $query = []): string
    {
        $url = Url::parse($base);

        foreach ($query as $key => $value) {
            $url->query->set($key, $value);
        }

        return (string) $url;
    }
}

Then Endpoint::for(config('services.billing.base_url'), ['key' => config('services.billing.key')]) from a job or HTTP client call.

Merge query parameters idempotently. This is the strongest day-to-day use. Because query->set() overwrites by key, you can layer defaults, then caller overrides, without duplicate-parameter bugs:

$url = Url::parse($request->input('callback_url'));
$url->query->set('signature', $signature);
$url->query->set('expires', now()->addHour()->timestamp);
$redirect = (string) $url;

Rewrite hosts for tenant/subdomain routing. Read the host, split on ., replace the leftmost label, write it back:

$url = Url::parse($webhookUrl);
$host = $url->get('host');               // e.g. "acme.example.com"
$url->set('host', "{$tenant->slug}.example.com");

Use the same technique to force a scheme (http → https) or to strip a port when you cross environments.

Path versioning for outbound APIs. When a vendor bumps /v1/ to /v2/, $url->path->set(...) (or a targeted string replace on (string) $url->path) is safer than regex against the whole URL, which can also hit the host or a query value.

Canonicalization / dedupe. Before storing URLs, parse and clear what you do not want: $url->set('fragment', null) (or ''), lowercase the host, drop tracking params with $url->query->remove('utm_source'). Cast to string and store that.

Feed it into Laravel's HTTP client. Purl has no HTTP opinion, which is fine — build the URL string, then hand it to Http::withToken(...)->get($url). Keep credential handling in the client, not in the URL, unless the upstream API genuinely requires query-string auth.

Testing. Assert on the string form, not on object internals: $this->assertSame('https://...', (string) $url);. It reads well and catches ordering/encoding surprises.

DI. If you want it injectable, bind a closure that returns a configured Url prototype, or better, bind your own EndpointBuilder interface in a service provider and keep Purl as an implementation detail.

Gotchas and Tips

  • It is archived (last release 2019-04-11). Assume no fixes. It was written for PHP 7.2+; on PHP 8.1/8.2+ expect possible deprecation notices and no compatibility guarantees. Pin the version, and if the surface you use is a handful of methods, consider vendoring or wrapping it so you can swap the implementation later.
  • Always cast to string. getQuery() and the query/path properties are objects, not strings. (string) $url->query and (string) $url are what you want; implicit concatenation is a common source of "Object of class Purl\Query could not be converted to string" errors or wrong output.
  • Encoding is not magic. Do not assume arbitrary unicode, spaces, &, or = in values survive round-tripping correctly. After building, log (string) $url and, if it matters, run the query values through urlencode() yourself. Test with a value containing a space and an ampersand before trusting it.
  • Arrays in the query. $url->query->set('tags', ['a','b']) is not tags[]=a&tags[]=b by default. For repeated parameters, either set distinct keys or drop to http_build_query() and $url->set('query', $qs).
  • Default ports. Setting an explicit port equal to the scheme default may or may not be elided depending on how you set it. If you care about exact output, normalize: clear the port for 80/443 and verify with a unit test.
  • Relative and partial URLs. Url::parse('/foo/bar') yields an object with null scheme/host. Guard get('host') and get('scheme') returns before branching on them — don't assume a URL string from user input is absolute.
  • Empty vs null parts. Setting fragment to '' and to null can render differently (...# vs no #). Pick one convention in your codebase and test it.
  • Do not replace url()/route() with this. Purl is for manipulating URLs you already have (third-party endpoints, stored callbacks, redirect targets). Laravel's URL generator remains the right tool for producing application routes; mixing the two invites subtle mismatches.
  • Extension points. Url, Path, and Query are ordinary classes you can subclass if you need behavior like always-lowercase hosts. Check the class hierarchy in vendor/jwage/purl/src before overriding; the base parts do most of the parsing work.
  • Debugging. dd((string) $url) — inspecting the object graph is noisy and easy to misread. For a quick sanity check in tinker, build the URL, cast it, and diff against the input string.
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.
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
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core