rowbot/url
A PHP implementation of the WHATWG URL Standard for parsing and manipulating URLs with spec-compliant accuracy. It handles internationalized domains, percent-encoding, relative resolution, and edge cases browsers agree on, offering a dependable alternative to PHP's parse_url for modern URL work.
Architecture fit. rowbot/url is a pure-PHP, dependency-light implementation of the WHATWG URL Standard. That is its entire value proposition and the reason to consider it: it parses URLs the way a browser does, not the way parse_url() (RFC 2396-flavored) or League\Uri (RFC 3986) does. For a Laravel application, the fit is narrow and specific. It does not replace the router, the URL facade, or asset generation — those operate on server-controlled, already-well-formed strings. It fits wherever the application ingests untrusted or externally authored URLs: webhook endpoints, redirect/callback targets, avatar and media URLs, feed sources, OAuth redirect_uri and return_to values, SSRF allow/deny checks, canonical cache keys, and outbound HTTP clients built on Guzzle.
The architectural mismatch to flag early is that Laravel's own request pipeline is not a consumer. Request::fullUrl(), route(), and url() are trusted by construction. Introducing a WHATWG parser there adds cost and a behavioral delta (trailing-slash and dot-segment normalization, default-port stripping) with no security or correctness payoff. Keep the package at the input boundary, not in the request lifecycle.
Integration feasibility. High, with a caveat. There is no Laravel service provider, facade, config file, or published integration — this is a framework-agnostic library and must be wrapped. That is a few hours of work, not a project: a UrlNormalizer adapter resolving through the container, a ValidUrl validation rule, optionally an Eloquent cast for persisted URL columns. The package's API is small and stable-shaped: construct URL with an input and optional base, read components via getters, manipulate query strings via URLSearchParams. The absence of first-party Laravel support is genuinely fine here because the surface area is tiny and the wrapper is where we own the contract anyway.
Technical risk. Three items dominate.
parse_url() for WHATWG semantics will change outputs on real inputs — http://example.com gains a trailing /, HTTP://Example.COM:80/ collapses to http://example.com/, backslashes in special-scheme URLs are treated as slashes, and some strings parse_url() accepts are rejected outright (and vice versa). Any existing validation rule or allowlist written against parse_url() must be re-derived, not silently repointed.false/null as parse_url() does. Every call site on a user-facing path must catch, or malformed submissions become 500s instead of validation failures.Key questions for the maintainer / spike.
ext-intl or a companion package — this affects our container image and CI matrix.URL and URLSearchParams mutable, and are they safe to reuse across a request or a queued job? (Assume mutable; therefore never register as a container singleton.)Stack fit. Requires Composer, no PHP extensions beyond a conventional baseline (verify IDN). No migrations, no config, no queues, no cache. It is one of the cheapest dependencies to add to a Laravel app and one of the easiest to remove — provided we do not let it leak across the codebase.
Wrapper first, always. Bind a UrlNormalizer interface in a service provider with the rowbot-backed implementation. The interface should expose only what the application needs — normalize(string $url, ?string $base = null): string, validate(...), isSameOrigin(...), withAllowedSchemes(...) — and return plain strings or a small immutable DTO, never a rowbot object. This is the single highest-leverage decision in the whole integration: it converts a bus-factor risk into a one-file swap, and it prevents the library's mutable types from becoming an accidental domain model.
Where to apply it, in priority order.
return_to, redirect_uri, post-login destinations) — highest security value, since normalization must happen before the allowlist comparison or the check is bypassable.ValidUrl validation rule used in FormRequests, replacing scattered filter_var(..., FILTER_VALIDATE_URL) and parse_url() checks.Where not to apply it. Route generation, asset URLs, request URL inspection, and anything operating on strings the framework produced.
Migration path. Run it side by side rather than as a cutover. Introduce the wrapper with a rowbot implementation, log the diff against the current parse_url()-based results in production for the input classes we care about, and only then flip the call sites. Where a legacy behavior is load-bearing (an allowlist that tolerates a malformed-but-working URL), preserve it explicitly in the adapter rather than inheriting it by accident.
Sequencing.
ValidUrl rule and apply it to redirect/callback validation and SSRF checks.parse_url() usage on untrusted input; leave framework-internal usage alone.Maintenance. Near-zero runtime maintenance: no services, no state, no configuration, no scheduled work. The maintenance cost is entirely dependency lifecycle — tracking a low-velocity, single-maintainer package, watching for a PHP minor that outpaces its releases, and being prepared to fork or replace. Budget a periodic review, not ongoing effort. The MIT license removes legal friction and permits a fork if the upstream goes dormant.
Support. Community-only, no SLA, no vendor. Escalation path is "read the spec and the source." Acceptable for a library this small and this well-scoped; unacceptable if we were to depend on it in a security-critical path without the wrapper — which is precisely why the wrapper is non-negotiable.
Scaling. Parsing is CPU-only and fast relative to the I/O that surrounds it; it will not be a throughput bottleneck. Two scaling notes: enforce a maximum input length before parsing so pathological URLs cannot become a cheap DoS vector on public endpoints, and memoize normalization where the same URLs recur in a hot path (cache keys, feed ingestion loops). Memory is trivial per call.
Failure modes.
Ramp-up. Low for anyone comfortable with URLs; the real learning curve is conceptual, not API. Engineers must internalize that WHATWG and RFC 3986 disagree, and that this package deliberately implements the former. Document the rule in one paragraph — "browser-parity parsing for untrusted input; framework helpers everywhere else" — and the team will use it correctly. Expect roughly a day for the first engineer to get productive and a short design note to prevent drift.
Overall. Technically sound, cheap to integrate, and narrowly but genuinely useful. The package's low adoption and single-maintainer profile are real risks, but they are contained by a thin adapter and a pinned version — and the WHATWG gap it fills is one PHP's native tooling leaves open. Recommend adoption scoped to untrusted-URL input boundaries, contingent on the corpus spike confirming that the behavioral delta is understood rather than merely tolerated.
How can I help you explore Laravel packages today?