Skip to content

feat: add immutable URI query variable helpers#10242

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

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

Conversation

@memleakd
Copy link
Copy Markdown
Contributor

@memleakd memleakd commented May 27, 2026

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'    => 'alice',
    'page' => 1,
    'role' => 'admin',
]);

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

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.

The naming follows the direction discussed in review: withQueryVar() / withQueryVars() are for merging into the current query, while future immutable full-query replacement can use names like withQuery() / withQueryArray().

Tests cover single and bulk query updates, replacing existing values, 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
Comment thread system/HTTP/URI.php Outdated
Comment thread system/HTTP/URI.php
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

@michalsn
Copy link
Copy Markdown
Member

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.

Yes, that's probably an oversight.

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

I'm also leaning towards this option.

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
@memleakd
Copy link
Copy Markdown
Contributor Author

Updated this to withQueryVar() / withQueryVars() and kept the null behavior unchanged from addQuery().

Thanks for helping work through the naming here.

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