juststeveking/uri-builder
juststeveking/uri-builder is a lightweight, fluent URI builder for PHP and Laravel. It lets you construct and manipulate URLs through a clean, chainable API for schemes, hosts, paths, query parameters, and fragments, avoiding fragile manual string concatenation. Ideal for consistent link generation.
composer require juststeveking/uri-builder. It is a pure Composer package — no service provider, facade, config, or migrations. You do not need to register anything in config/app.php or a provider.README.md, then composer.json for the PHP constraint, then src/ and tests/. This codebase is tiny (single value object plus a builder); reading the source is faster and more reliable than inferring from examples. Confirm the actual exported class name and method names in the version you install before writing app code.sprintf('%s://%s/%s?%s', ...) or .=-style concatenation.Typical shape (verify exact method names against your installed version):
use JustSteveKing\UriBuilder\Uri;
$uri = Uri::make()
->withScheme('https')
->withHost('api.example.com')
->withPath('/v1/webhooks')
->withQuery(['tenant' => 'acme', 'page' => 2]);
echo (string) $uri;
// https://api.example.com/v1/webhooks?tenant=acme&page=2
Everything is immutable, so each with* returns a new instance. That means the first habit to build is capturing the return value:
$base = Uri::make()->withScheme('https')->withHost('api.example.com');
$endpoint = $base->withPath('/v1/orders'); // $base is unchanged
Wrap it before you spread it. Because there is no Laravel integration, the correct pattern is a one-file internal factory that exposes only the components you need. This is also your exit ramp if the package is abandoned.
namespace App\Support;
use JustSteveKing\UriBuilder\Uri;
interface EndpointBuilder
{
public function build(string $path, array $query = []): string;
}
final class UriEndpointBuilder implements EndpointBuilder
{
public function __construct(private string $scheme, private string $host) {}
public function build(string $path, array $query = []): string
{
return (string) Uri::make()
->withScheme($this->scheme)
->withHost($this->host)
->withPath($path)
->withQuery($query);
}
}
Bind the interface in a provider, keep the package behind it, and a future swap to league/uri, PSR-7, or Laravel 11's native Illuminate\Support\Uri is a contained change rather than a grep-and-replace across controllers, jobs, and mailables.
Reuse an immutable base URI. The value-object design makes "tenant base + endpoint path" clean. Resolve the base once per tenant/environment, then derive endpoints from it without defensive copying:
$tenantBase = Uri::make()
->withScheme(config('services.crm.scheme'))
->withHost($tenant->api_host)
->withPort($tenant->api_port);
Config-driven hosts and environment awareness. Put scheme/host/port/base-path in config/services.php and build the Uri from that config. Do not string-interpolate hostnames into paths — component-first construction is the whole point.
Query strings belong in withQuery(), not in the path string. Keeping the path and query separated is what lets the builder handle encoding once. Treat array query values as a thing to verify (see gotchas) rather than assuming http_build_query parity.
Use it only for outbound construction. Leave route(), url(), action(), and signed URLs to Laravel. This package has no route-name awareness, no locale handling, and no signature integration — using it there is a regression, not an abstraction.
Pin behavior with characterization tests. Before adopting in more than one place, write tests that assert the exact output string for your top edge cases: values containing spaces, &, #, =, ?, unicode, and already-percent-encoded input. These tests are what let you upgrade, fork, or swap the implementation deliberately instead of discovering a change in production.
Pilot in one bounded surface. Pick a single non-security-critical path (a webhook dispatcher or a reporting endpoint builder), ship it, then expand after a release cycle without incident.
composer.json and assume you own patching. MIT licensing means forking or vendoring the (small) source is a legitimate fallback — keep the adapter seam so it stays cheap.http_build_query() parity. Specifically verify: space encoding (%20 vs +), reserved characters (&, #, =, ?) inside query values, repeated query keys, array values, unicode/IDN hosts, default ports (does https + port 443 get elided?), IPv6 hosts, empty paths, and trailing slashes. Mis-encoding on user-controlled input is a correctness and injection concern, not a cosmetic one.composer.json against your matrix (8.2/8.3/8.4). A 2022 constraint may not include newer PHP, and no upstream fix may be coming — you may need composer.json overrides or a fork.league/uri territory. Confirm in the source whether it even accepts an existing URI string.UriInterface, so it cannot be handed to Guzzle, Symfony HTTP, middleware, or anything expecting a PSR-7 URI. Convert at the boundary if you need both.app(EndpointBuilder::class) only works because you bound your wrapper. Do not sprinkle new Uri/Uri::make() through controllers; that defeats the seam and makes the swap expensive.dd((string) $uri); early, and compare side-by-side against http_build_query($params). When output diverges, that divergence is either a deliberate opinionation (document it) or a bug in your usage — the comparison makes it obvious which.%20 and seeing whether it becomes %2520.Uri and adds domain helpers (forTenant(), withCursor()). Avoid patching the package in vendor/; fork it if you truly need to change behavior, so it is reviewable and versioned.How can I help you explore Laravel packages today?