Skip to content

Commit 79cbf3d

Browse files
committed
feat(http): add immutable URI query var helpers
- 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>
1 parent aad4c9d commit 79cbf3d

5 files changed

Lines changed: 139 additions & 0 deletions

File tree

system/HTTP/URI.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,56 @@ public function addQuery(string $key, $value = null)
875875
return $this;
876876
}
877877

878+
/**
879+
* Return an instance with one query var added, replaced, or removed.
880+
*
881+
* Note: Method not in PSR-7
882+
*
883+
* @param int|string|null $value Null removes the query var.
884+
*
885+
* @return static
886+
*/
887+
public function withQueryVar(string $key, $value)
888+
{
889+
$uri = clone $this;
890+
891+
if ($value === null) {
892+
unset($uri->query[$key]);
893+
894+
return $uri;
895+
}
896+
897+
$uri->query[$key] = $value;
898+
899+
return $uri;
900+
}
901+
902+
/**
903+
* Return an instance with multiple query vars added, replaced, or removed.
904+
*
905+
* Note: Method not in PSR-7
906+
*
907+
* @param array<string, int|string|null> $params Null values remove query vars.
908+
*
909+
* @return static
910+
*/
911+
public function withQueryVars(array $params)
912+
{
913+
$uri = clone $this;
914+
915+
foreach ($params as $key => $value) {
916+
if ($value === null) {
917+
unset($uri->query[$key]);
918+
919+
continue;
920+
}
921+
922+
$uri->query[$key] = $value;
923+
}
924+
925+
return $uri;
926+
}
927+
878928
/**
879929
* Removes one or more query vars from the URI.
880930
*

tests/system/HTTP/URITest.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,65 @@ public function testAddQueryVarRespectsExistingQueryVars(): void
835835
$this->assertSame('http://example.com/foo?bar=baz&baz=foz', (string) $uri);
836836
}
837837

838+
public function testWithQueryVarAddsQueryVarWithoutMutatingOriginal(): void
839+
{
840+
$base = 'http://example.com/foo';
841+
$uri = new URI($base);
842+
843+
$new = $uri->withQueryVar('bar', 'baz');
844+
845+
$this->assertSame('http://example.com/foo?bar=baz', (string) $new);
846+
$this->assertSame('http://example.com/foo', (string) $uri);
847+
}
848+
849+
public function testWithQueryVarReplacesQueryVarAndPreservesFragment(): void
850+
{
851+
$base = 'http://example.com/foo?bar=baz#section';
852+
$uri = new URI($base);
853+
854+
$new = $uri->withQueryVar('bar', 'foz');
855+
856+
$this->assertSame('http://example.com/foo?bar=foz#section', (string) $new);
857+
$this->assertSame('http://example.com/foo?bar=baz#section', (string) $uri);
858+
}
859+
860+
public function testWithQueryVarRemovesQueryVarWhenValueIsNull(): void
861+
{
862+
$base = 'http://example.com/foo?foo=bar&bar=baz&baz=foz';
863+
$uri = new URI($base);
864+
865+
$new = $uri->withQueryVar('bar', null);
866+
867+
$this->assertSame('http://example.com/foo?foo=bar&baz=foz', (string) $new);
868+
$this->assertSame('http://example.com/foo?foo=bar&bar=baz&baz=foz', (string) $uri);
869+
}
870+
871+
public function testWithQueryVarKeepsEmptyStringQueryVar(): void
872+
{
873+
$base = 'http://example.com/foo?bar=baz';
874+
$uri = new URI($base);
875+
876+
$new = $uri->withQueryVar('bar', '');
877+
878+
$this->assertSame('http://example.com/foo?bar=', (string) $new);
879+
$this->assertSame('http://example.com/foo?bar=baz', (string) $uri);
880+
}
881+
882+
public function testWithQueryVarsAddsReplacesAndRemovesWithoutMutatingOriginal(): void
883+
{
884+
$base = 'http://example.com/foo?foo=bar&bar=baz&baz=foz#section';
885+
$uri = new URI($base);
886+
887+
$new = $uri->withQueryVars([
888+
'bar' => null,
889+
'baz' => 'updated',
890+
'new' => 'value',
891+
]);
892+
893+
$this->assertSame('http://example.com/foo?foo=bar&baz=updated&new=value#section', (string) $new);
894+
$this->assertSame('http://example.com/foo?foo=bar&bar=baz&baz=foz#section', (string) $uri);
895+
}
896+
838897
public function testStripQueryVars(): void
839898
{
840899
$base = 'http://example.com/foo?foo=bar&bar=baz&baz=foz';

user_guide_src/source/changelogs/v4.8.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ HTTP
276276
- ``CLIRequest`` now supports options with values specified using an equals sign (e.g., ``--option=value``) in addition to the existing space-separated syntax (e.g., ``--option value``).
277277
This provides more flexibility in how you can pass options to CLI requests.
278278
- Added ``$enableStyleNonce`` and ``$enableScriptNonce`` options to ``Config\App`` to automatically add nonces to control whether to add nonces to style-* and script-* directives in the Content Security Policy (CSP) header when CSP is enabled. See :ref:`csp-control-nonce-generation` for details.
279+
- Added ``URI::withQueryVar()`` and ``URI::withQueryVars()`` to return a cloned URI with query variables added, replaced, or removed.
279280
- ``URI`` now accepts an optional boolean second parameter in the constructor, defaulting to ``false``, to control how the query string is parsed in instantiation.
280281
This is the behavior of ``->useRawQueryString()`` brought into the constructor for convenience. Previously, you need to call ``$uri->useRawQueryString(true)->setURI($uri)`` to get this behavior.
281282
Now you can simply do ``new URI($uri, true)``.

user_guide_src/source/libraries/uri.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,19 @@ parameter is the name of the variable, and the second parameter is the value:
188188

189189
.. literalinclude:: uri/019.php
190190

191+
Changing Query Values Without Mutation
192+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
193+
194+
.. versionadded:: 4.8.0
195+
196+
You can return a new URI instance with one or more query variables changed by using the ``withQueryVar()``
197+
and ``withQueryVars()`` methods. Existing query variables are preserved unless they are replaced or removed.
198+
Passing ``null`` removes a query variable:
199+
200+
.. literalinclude:: uri/028.php
201+
202+
The original URI instance is not modified.
203+
191204
Filtering Query Values
192205
^^^^^^^^^^^^^^^^^^^^^^
193206

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
$uri = new \CodeIgniter\HTTP\URI('https://example.com/users?q=bob&page=1');
4+
5+
$nextPage = $uri->withQueryVar('page', 2);
6+
// https://example.com/users?q=bob&page=2
7+
8+
$withoutSearch = $uri->withQueryVar('q', null);
9+
// https://example.com/users?page=1
10+
11+
$filtered = $uri->withQueryVars([
12+
'q' => null,
13+
'page' => 1,
14+
'role' => 'admin',
15+
]);
16+
// https://example.com/users?page=1&role=admin

0 commit comments

Comments
 (0)