Skip to content

feat: add immutable URI query variable helpers#10242

Open
memleakd wants to merge 2 commits into
codeigniter4:4.8from
memleakd:feat/uri-query-param-mutation
Open

feat: add immutable URI query variable helpers#10242
memleakd wants to merge 2 commits into
codeigniter4:4.8from
memleakd:feat/uri-query-param-mutation

Conversation

@memleakd
Copy link
Copy Markdown
Contributor

Description

This PR proposes adding two small helpers to URI for changing query variables without mutating the current instance:

$nextPage = $uri->withQueryVar('page', 2);

$filtered = $uri->withQueryVars([
    'q'    => null,
    'page' => 1,
    'role' => 'admin',
]);

Both methods return a cloned URI. Existing query variables are preserved, matching keys are replaced, new keys are added, and null removes a key.

This is handy for everyday URL-building work: pagination links, filter links, canonical URLs, and other places where code needs to adjust a URI while preserving the original instance.

Quick note on naming: I originally implemented this as withQueryParam() / withQueryParams(), but later realized URI.php already has TODO tags for withQuery() and withQueryParams() as future immutable versions of setQuery() and setQueryArray(). Using withQueryVar() / withQueryVars() keeps those future names free for full-query replacement semantics.

Tests cover single and bulk query updates, replacing existing values, removing values with null, preserving empty strings, preserving fragments, and keeping the original URI unchanged.

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

- Add withQueryVar() and withQueryVars() for cloned query updates
- Support adding, replacing, and removing query variables
- Document immutable query variable updates
- Add tests for single and bulk query changes

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
@github-actions github-actions Bot added the 4.8 PRs that target the `4.8` branch. label May 27, 2026
Comment thread system/HTTP/URI.php Outdated
Comment thread system/HTTP/URI.php Outdated
Comment thread system/HTTP/URI.php Outdated
Comment thread system/HTTP/URI.php Outdated
Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
@michalsn
Copy link
Copy Markdown
Member

I still have some doubts about the withAddedQueryVar() / withAddedQueryVars() naming.

When proposing this, I intended to mirror the PSR-7 withHeader() / withAddedHeader() distinction, where one method replaces and the other preserves the existing state. But for headers, "added" really means preserving existing values and appending another value. So the naming is not a perfect semantic match.

My current thinking is that we should evaluate the names in terms of the planned transition to a PSR-7-style immutable UriInterface, where existing mutable methods are deprecated and later removed, not silently changed in place.

v4 mutator Semantics v5 immutable
setQuery(string) Replace from string withQuery(string) PSR-7 method
setQueryArray(array) Replace from array withQueryVars(array)
addQuery(string $key, $value) Merge one query variable withAddedQueryVar(string $key, $value)
New method Merge many query variables withAddedQueryVars(array $vars)
stripQuery(...$keys) Remove query variables withoutQueryVars(...$keys)
keepQuery(...$keys) Keep only selected query variables withOnlyQueryVars(...$keys)

In that full API family, the replace-vs-merge distinction becomes clearer:

$uri = $uri->withQueryVars([
    'page' => 1,
]);

replaces the query variable collection, while:

$uri = $uri->withAddedQueryVars([
    'sort' => 'name',
]);

preserves the existing query variables and merges the provided ones into them.

That said, I am not fully convinced that withAddedQueryVar() / withAddedQueryVars() is the best name, because "added" can (as I already mentioned) imply appending rather than replacing an existing key.

So maybe we should follow this transition path (which is basically the initial PR):

v4 mutator Semantics v5 immutable
setQuery(string) Replace from string withQuery(string $query)
setQueryArray(array) Replace from array withQueryArray(array $query)
addQuery(string $key, $value) Merge one query variable withQueryVar(string $key, $value)
New method Merge many query variables withQueryVars(array $vars)
stripQuery(...$keys) Remove query variables withoutQueryVars(...$keys)
keepQuery(...$keys) Keep only selected query variables withOnlyQueryVars(...$keys)

Looking forward to some feedback, because at this point I have been thinking about this for too long and IDK which option is better.

@michalsn michalsn requested a review from paulbalandan May 29, 2026 20:28
@memleakd
Copy link
Copy Markdown
Contributor Author

Thanks for laying it out this way. After looking at PSR-7 again, I think the second option feels cleaner to me too.

The main thing that makes me hesitate about withAddedQueryVar() is the same one you mentioned: "added" works nicely for headers, but query vars are a key/value map, so replacing an existing key through an "added" method feels a little strange.

I also noticed that PSR-7 already uses withQueryParams() on ServerRequestInterface, not UriInterface, and it explicitly does not change the URI. Since CI4's RequestInterface also points toward that interface, I think avoiding URI::withQueryParams() is safer.

So my preference would be:

  • withQueryArray() for replacing the full query array
  • withQueryVar() / withQueryVars() for merging into the current query
  • keep delete-via-null out

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.8 PRs that target the `4.8` branch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants