league/uri-manipulations
League URI Manipulations provides immutable, PSR-7-friendly tools for modifying URIs in PHP. Add, remove, update, or conditionally alter query parameters, paths, and other components through a clean, composable API.
league/uri-manipulations is a small, function-based companion to the League URI stack. It takes any PSR-7 UriInterface and returns a new immutable instance with one targeted change applied (query param added/removed, path segment appended, trailing slash toggled, etc.). It is not a router, a builder DSL, or a URL generator — it is a set of safe, encoding-aware string surgery helpers.
Minimal setup:
composer require league/uri-manipulations:^2.0
It pulls in league/uri-components and psr/http-message. No service provider, no config file, no facade — the functions are autoloaded via Composer files, so once installed they are callable immediately.
Where to look first: the README's function table (it lists every function and its one-line contract) and the tests/ directory, which doubles as the most accurate documentation of edge-case behaviour (empty query strings, root paths, encoded delimiters).
First use case — the one you will hit on day one:
use League\Uri\Uri;
use function League\Uri\Manipulations\append_query;
$uri = Uri::createFromString('https://example.com/search?q=shoes');
echo append_query($uri, 'page=2');
// https://example.com/search?q=shoes&page=2
The value proposition: you never concatenate '?', '&', or '/' yourself, and you never accidentally double-encode a value that the query parser already owns.
Tracking / UTM decoration at the edge. Wrap the decoration in one helper so you can swap the implementation later:
function withCampaign(UriInterface $uri, string $source): UriInterface
{
return merge_query($uri, ['utm_source' => $source]);
}
Prefer merge_query over append_query for anything keyed — merge replaces an existing key, append will happily produce ?page=1&page=2.
Pagination links. Take the current request URI straight off the PSR-7 request and derive page links without re-parsing anything:
$next = merge_query($request->getUri(), ['page' => $current + 1]);
$prev = $current > 1
? merge_query($request->getUri(), ['page' => $current - 1])
: remove_query($request->getUri(), 'page');
Canonical URLs and cache keys. sort_query gives you a deterministic query ordering, and remove_query strips churn-inducing params (utm_*, fbclid, session ids). The pair is a compact canonicaliser:
$canonical = (string) sort_query(remove_query($uri, ['fbclid', 'gclid']));
$cacheKey = sha1($canonical);
API base URLs and link assembly. append_path / append_segment are the safe way to join a base endpoint with a resource id, because they respect whether the base already ends in / and re-encode the segment:
$endpoint = append_segment(Uri::createFromString('https://api.example.com/v1'), 'users');
// https://api.example.com/v1/users
Framework glue. Put the functions behind one class (or a Twig/Blade helper) so application code depends on your interface, not on a set of global functions from an archived package. This is also the seam you will later reimplement against league/uri v6+.
Composition order matters and is usually: normalise → mutate → serialise. remove_trailing_slash before appending a segment avoids // in the path; sort_query last before hashing.
league/uri-components 5.x. It generally still runs, but treat it as vendored code: pin the exact version, and plan the migration to League\Uri\Uri + League\Uri\Components\Query/Path (which absorbed these operations as methods: Query::merge(), Uri::withPath(), etc.).append_query($uri, 'a=1'); followed by echo $uri; silently does nothing. Always assign.append_query duplicates keys. If you might touch an existing parameter, use merge_query. Duplicate-key query strings are legal but are a classic source of "why is page 1 and 2?" bugs.append_query($uri, 'q=shoes & boots')) produces an invalid URI rather than an exception — validate with Uri::createFromString on the result in tests./ and never touch the host or fragment. If you expect https://example.com → https://example.com/, that is withPath('/') territory, not this function.remove_query on the last remaining parameter may leave a dangling ? depending on version; normalise by round-tripping through Uri::createFromString((string) $uri) if the exact serialisation matters (e.g. signed URLs — where you should not be using this package at all).#frag before ?query as invalid. Sanity-check anything you generate for redirects.append_path does not implement RFC 3986 merge semantics. append_path($base, '../x') will not do what you want; resolve against a base URI yourself first.var_dump((string) $before, (string) $after) on the two URIs and diff the components individually (getScheme, getHost, getPath, getQuery, getFragment). Almost every reported "bug" is the wrong component being mutated.How can I help you explore Laravel packages today?