diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index ef7c8d6acfca..3b8f9b7ae4e9 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -875,6 +875,56 @@ public function addQuery(string $key, $value = null) return $this; } + /** + * Return an instance with one query var added, replaced, or removed. + * + * Note: Method not in PSR-7 + * + * @param int|string|null $value Null removes the query var. + * + * @return static + */ + public function withQueryVar(string $key, $value) + { + $uri = clone $this; + + if ($value === null) { + unset($uri->query[$key]); + + return $uri; + } + + $uri->query[$key] = $value; + + return $uri; + } + + /** + * Return an instance with multiple query vars added, replaced, or removed. + * + * Note: Method not in PSR-7 + * + * @param array $params Null values remove query vars. + * + * @return static + */ + public function withQueryVars(array $params) + { + $uri = clone $this; + + foreach ($params as $key => $value) { + if ($value === null) { + unset($uri->query[$key]); + + continue; + } + + $uri->query[$key] = $value; + } + + return $uri; + } + /** * Removes one or more query vars from the URI. * diff --git a/tests/system/HTTP/URITest.php b/tests/system/HTTP/URITest.php index 8dda3a521810..16d58abf00d6 100644 --- a/tests/system/HTTP/URITest.php +++ b/tests/system/HTTP/URITest.php @@ -835,6 +835,65 @@ public function testAddQueryVarRespectsExistingQueryVars(): void $this->assertSame('http://example.com/foo?bar=baz&baz=foz', (string) $uri); } + public function testWithQueryVarAddsQueryVarWithoutMutatingOriginal(): void + { + $base = 'http://example.com/foo'; + $uri = new URI($base); + + $new = $uri->withQueryVar('bar', 'baz'); + + $this->assertSame('http://example.com/foo?bar=baz', (string) $new); + $this->assertSame('http://example.com/foo', (string) $uri); + } + + public function testWithQueryVarReplacesQueryVarAndPreservesFragment(): void + { + $base = 'http://example.com/foo?bar=baz#section'; + $uri = new URI($base); + + $new = $uri->withQueryVar('bar', 'foz'); + + $this->assertSame('http://example.com/foo?bar=foz#section', (string) $new); + $this->assertSame('http://example.com/foo?bar=baz#section', (string) $uri); + } + + public function testWithQueryVarRemovesQueryVarWhenValueIsNull(): void + { + $base = 'http://example.com/foo?foo=bar&bar=baz&baz=foz'; + $uri = new URI($base); + + $new = $uri->withQueryVar('bar', null); + + $this->assertSame('http://example.com/foo?foo=bar&baz=foz', (string) $new); + $this->assertSame('http://example.com/foo?foo=bar&bar=baz&baz=foz', (string) $uri); + } + + public function testWithQueryVarKeepsEmptyStringQueryVar(): void + { + $base = 'http://example.com/foo?bar=baz'; + $uri = new URI($base); + + $new = $uri->withQueryVar('bar', ''); + + $this->assertSame('http://example.com/foo?bar=', (string) $new); + $this->assertSame('http://example.com/foo?bar=baz', (string) $uri); + } + + public function testWithQueryVarsAddsReplacesAndRemovesWithoutMutatingOriginal(): void + { + $base = 'http://example.com/foo?foo=bar&bar=baz&baz=foz#section'; + $uri = new URI($base); + + $new = $uri->withQueryVars([ + 'bar' => null, + 'baz' => 'updated', + 'new' => 'value', + ]); + + $this->assertSame('http://example.com/foo?foo=bar&baz=updated&new=value#section', (string) $new); + $this->assertSame('http://example.com/foo?foo=bar&bar=baz&baz=foz#section', (string) $uri); + } + public function testStripQueryVars(): void { $base = 'http://example.com/foo?foo=bar&bar=baz&baz=foz'; diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 896b2552aa83..15652710244c 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -276,6 +276,7 @@ HTTP - ``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``). This provides more flexibility in how you can pass options to CLI requests. - 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. +- Added ``URI::withQueryVar()`` and ``URI::withQueryVars()`` to return a cloned URI with query variables added, replaced, or removed. - ``URI`` now accepts an optional boolean second parameter in the constructor, defaulting to ``false``, to control how the query string is parsed in instantiation. 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. Now you can simply do ``new URI($uri, true)``. diff --git a/user_guide_src/source/libraries/uri.rst b/user_guide_src/source/libraries/uri.rst index ce66cd695da5..3a3f277a980c 100644 --- a/user_guide_src/source/libraries/uri.rst +++ b/user_guide_src/source/libraries/uri.rst @@ -188,6 +188,19 @@ parameter is the name of the variable, and the second parameter is the value: .. literalinclude:: uri/019.php +Changing Query Values Without Mutation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 4.8.0 + +You can return a new URI instance with one or more query variables changed by using the ``withQueryVar()`` +and ``withQueryVars()`` methods. Existing query variables are preserved unless they are replaced or removed. +Passing ``null`` removes a query variable: + +.. literalinclude:: uri/028.php + +The original URI instance is not modified. + Filtering Query Values ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/user_guide_src/source/libraries/uri/028.php b/user_guide_src/source/libraries/uri/028.php new file mode 100644 index 000000000000..bd6a4fd4a284 --- /dev/null +++ b/user_guide_src/source/libraries/uri/028.php @@ -0,0 +1,16 @@ +withQueryVar('page', 2); +// https://example.com/users?q=bob&page=2 + +$withoutSearch = $uri->withQueryVar('q', null); +// https://example.com/users?page=1 + +$filtered = $uri->withQueryVars([ + 'q' => null, + 'page' => 1, + 'role' => 'admin', +]); +// https://example.com/users?page=1&role=admin