diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index ab8772b55a8f..f8921af61a2c 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -817,8 +817,6 @@ protected function refreshPath(): self * to clean the various parts of the query keys and values. * * @return $this - * - * @TODO PSR-7: Should be `withQuery($query)`. */ public function setQuery(string $query) { @@ -844,13 +842,23 @@ public function setQuery(string $query) return $this; } + /** + * Returns an instance with the specified query string. + */ + public function withQuery(string $query): static + { + $uri = clone $this; + + $uri->setQuery($query); + + return $uri; + } + /** * A convenience method to pass an array of items in as the Query * portion of the URI. * - * @return URI - * - * @TODO: PSR-7: Should be `withQueryParams(array $query)` + * @return $this */ public function setQueryArray(array $query) { @@ -859,6 +867,22 @@ public function setQueryArray(array $query) return $this->setQuery($query); } + /** + * Returns an instance with the specified query vars. + * + * Note: Method not in PSR-7 + * + * @param array $query + */ + public function withQueryArray(array $query): static + { + $uri = clone $this; + + $uri->setQueryArray($query); + + return $uri; + } + /** * Adds a single new element to the query vars. * @@ -925,6 +949,20 @@ public function stripQuery(...$params) return $this; } + /** + * Returns an instance without the specified query vars. + * + * Note: Method not in PSR-7 + */ + public function withoutQueryVars(string ...$params): static + { + $uri = clone $this; + + $uri->stripQuery(...$params); + + return $uri; + } + /** * Filters the query variables so that only the keys passed in * are kept. The rest are removed from the object. @@ -952,6 +990,20 @@ public function keepQuery(...$params) return $this; } + /** + * Returns an instance with only the specified query vars. + * + * Note: Method not in PSR-7 + */ + public function withOnlyQueryVars(string ...$params): static + { + $uri = clone $this; + + $uri->keepQuery(...$params); + + return $uri; + } + /** * Sets the fragment portion of the URI. * diff --git a/tests/system/HTTP/URITest.php b/tests/system/HTTP/URITest.php index 11eb472951d7..dc81928f1685 100644 --- a/tests/system/HTTP/URITest.php +++ b/tests/system/HTTP/URITest.php @@ -477,6 +477,19 @@ public function testSetQuerySetsValue(): void $this->assertSame($expected, (string) $uri); } + public function testWithQuerySetsQueryWithoutMutatingOriginal(): void + { + $url = 'http://example.com/path?foo=bar#fragment'; + $uri = new URI($url); + + $new = $uri->withQuery('?key=value&second.key=value.2'); + + $this->assertNotSame($uri, $new); + $this->assertSame('key=value&second_key=value.2', $new->getQuery()); + $this->assertSame('http://example.com/path?key=value&second_key=value.2#fragment', (string) $new); + $this->assertSame($url, (string) $uri); + } + public function testUseRawQueryStringAtConstructor(): void { $url = 'http://example.com/path?key=value&second.key=value.2'; @@ -509,6 +522,32 @@ public function testSetQueryArraySetsValue(): void $this->assertSame($expected, (string) $uri); } + public function testWithQueryArraySetsQueryWithoutMutatingOriginal(): void + { + $url = 'http://example.com/path?foo=bar#fragment'; + $uri = new URI($url); + + $new = $uri->withQueryArray(['key' => 'value', 'second.key' => 'value.2']); + + $this->assertNotSame($uri, $new); + $this->assertSame('key=value&second_key=value.2', $new->getQuery()); + $this->assertSame('http://example.com/path?key=value&second_key=value.2#fragment', (string) $new); + $this->assertSame($url, (string) $uri); + } + + public function testWithQueryArraySetsQueryWithUseRawQueryStringWithoutMutatingOriginal(): void + { + $url = 'http://example.com/path?foo=bar#fragment'; + $uri = new URI($url, true); + + $new = $uri->withQueryArray(['key' => 'value', 'second.key' => 'value.2']); + + $this->assertNotSame($uri, $new); + $this->assertSame('key=value&second.key=value.2', $new->getQuery()); + $this->assertSame('http://example.com/path?key=value&second.key=value.2#fragment', (string) $new); + $this->assertSame($url, (string) $uri); + } + public function testSetQueryArraySetsValueWithUseRawQueryString(): void { $url = 'http://example.com/path'; @@ -531,6 +570,20 @@ public function testSetQueryThrowsErrorWhenFragmentPresent(): void $uri->setQuery('?key=value#fragment'); } + public function testWithQueryThrowsErrorWhenFragmentPresentWithoutMutatingOriginal(): void + { + $url = 'http://example.com/path?foo=bar'; + $uri = new URI($url); + + $this->expectException(HTTPException::class); + + try { + $uri->withQuery('?key=value#fragment'); + } finally { + $this->assertSame($url, (string) $uri); + } + } + /** * @param string $url * @param string $expected @@ -896,6 +949,18 @@ public function testStripQueryVars(): void $this->assertSame('http://example.com/foo?foo=bar', (string) $uri); } + public function testWithoutQueryVarsRemovesQueryVarsWithoutMutatingOriginal(): void + { + $base = 'http://example.com/foo?foo=bar&bar=baz&baz=foz#section'; + $uri = new URI($base); + + $new = $uri->withoutQueryVars('bar', 'baz'); + + $this->assertNotSame($uri, $new); + $this->assertSame('http://example.com/foo?foo=bar#section', (string) $new); + $this->assertSame($base, (string) $uri); + } + public function testKeepQueryVars(): void { $base = 'http://example.com/foo?foo=bar&bar=baz&baz=foz'; @@ -906,6 +971,18 @@ public function testKeepQueryVars(): void $this->assertSame('http://example.com/foo?bar=baz&baz=foz', (string) $uri); } + public function testWithOnlyQueryVarsKeepsQueryVarsWithoutMutatingOriginal(): void + { + $base = 'http://example.com/foo?foo=bar&bar=baz&baz=foz#section'; + $uri = new URI($base); + + $new = $uri->withOnlyQueryVars('bar', 'baz'); + + $this->assertNotSame($uri, $new); + $this->assertSame('http://example.com/foo?bar=baz&baz=foz#section', (string) $new); + $this->assertSame($base, (string) $uri); + } + public function testEmptyQueryVars(): void { $base = 'http://example.com/foo'; diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 82f4fbf95d21..1093bda40039 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -287,6 +287,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::withQuery()``, ``URI::withQueryArray()``, ``URI::withoutQueryVars()``, and ``URI::withOnlyQueryVars()`` to return cloned URIs with replaced or filtered query variables. - Added ``URI::withQueryVar()`` and ``URI::withQueryVars()`` to return a cloned URI with query variables added or replaced. - ``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. diff --git a/user_guide_src/source/libraries/uri.rst b/user_guide_src/source/libraries/uri.rst index 6d9179871676..e60ba0d4e06b 100644 --- a/user_guide_src/source/libraries/uri.rst +++ b/user_guide_src/source/libraries/uri.rst @@ -193,12 +193,22 @@ Changing Query Values Without Mutation .. versionadded:: 4.8.0 +You can return a new URI instance with its query variables replaced by using the +``withQuery()`` and ``withQueryArray()`` methods: + +.. literalinclude:: uri/029.php + You can return a new URI instance with one or more query variables added or replaced by using the ``withQueryVar()`` and ``withQueryVars()`` methods. Existing query variables are preserved unless they are replaced: .. literalinclude:: uri/028.php +You can also return a new URI instance with query variables removed or filtered by using the +``withoutQueryVars()`` and ``withOnlyQueryVars()`` methods: + +.. literalinclude:: uri/030.php + The original URI instance is not modified. Filtering Query Values diff --git a/user_guide_src/source/libraries/uri/029.php b/user_guide_src/source/libraries/uri/029.php new file mode 100644 index 000000000000..14af204241bd --- /dev/null +++ b/user_guide_src/source/libraries/uri/029.php @@ -0,0 +1,12 @@ +withQuery('page=2'); +// https://example.com/users?page=2 + +$filtered = $uri->withQueryArray([ + 'q' => 'alice', + 'role' => 'admin', +]); +// https://example.com/users?q=alice&role=admin diff --git a/user_guide_src/source/libraries/uri/030.php b/user_guide_src/source/libraries/uri/030.php new file mode 100644 index 000000000000..c1906a0bdcf0 --- /dev/null +++ b/user_guide_src/source/libraries/uri/030.php @@ -0,0 +1,9 @@ +withoutQueryVars('page'); +// https://example.com/users?q=bob&role=admin + +$onlyFilters = $uri->withOnlyQueryVars('q', 'role'); +// https://example.com/users?q=bob&role=admin