Take the State of PHP 2026 Survey →

Query Parameter Manipulation Support View as Markdown

Under discussion
▴ 0 ▾ 0
community vote
Authors
Máté Kocsis, kocsismate@php.net
Date
2026-02-13
Version
1.0
Implementation
https://github.com/php/php-src/pull/22551
Source
wiki.php.net/rfc/query_params

Vote

Add support for query parameter manipulation as outlined in the RFC? no votes
Yes · No · Abstain

Discussions on the mailing list

RFC text

Copied from wiki.php.net, last synchronized Oct 13, 2026 11:49 UTC

Introduction

Despite being the web's most prominent server-side language for over 30 years, PHP still lacks a dedicated object-oriented API for working with query parameters. The existing tools — the $_GET superglobal, parse_str(), and http_build_query() — cover common parsing and building tasks, but do not expose query parameters as an ordered list of name-value pairs. Their behavior follows RFC 1866's application/x-www-form-urlencoded, while only http_build_query() supports RFC 3986 percent-encoding.

A request-independent, immutable query parameter list representation is particularly useful in modern PHP applications, including application server models such as FrankenPHP or Swoole, where a single PHP process handles multiple requests over its lifetime. Superglobals expose mutable state associated with the current request. A dedicated value object allows query parameters to be passed explicitly between application components and used independently of that global state, consistently with modern PSR-7-based request abstractions.

This RFC introduces Uri\QueryParams — a generic object-oriented abstraction for parsing, inspecting, modifying, and recomposing query parameters.

Proposal

The following classes and methods are proposed for addition:

namespace Uri {
    final readonly class QueryParamParsingOptions
    {
        public int $maxQueryStringLength;
        public int $maxParamCount;
 
        /**
         * @throws \ValueError
         */
        public function __construct(
            int $maxQueryStringLength = 10000,
            int $maxParamCount = 1000,
        ) {}
    }
 
    enum QueryParamNullPolicy
    {
        case SkipTuple;
        case KeyOnly;
        case EmptyString;
    }
 
    final readonly class QueryParamBuildingOptions
    {
        public string $trueValue;
        public string $falseValue;
        public \Uri\QueryParamNullPolicy $nullPolicy;
 
        public function __construct(
            string $trueValue = "1",
            string $falseValue = "0",
            \Uri\QueryParamNullPolicy $nullPolicy = \Uri\QueryParamNullPolicy::KeyOnly
        ) {}
    }
 
    class InvalidQueryParamsException extends \Uri\UriException
    {
    }
}
namespace Uri {
    final readonly class QueryParams implements \IteratorAggregate, \Countable
    {
        private \Uri\QueryParamBuildingOptions $buildingOptions;
 
        /**
         * @throws \Uri\InvalidQueryParamsException
         */  
        public static function parseRfc1866(
            string $queryString,
            ?\Uri\QueryParamParsingOptions $parsingOptions = null,
            ?\Uri\QueryParamBuildingOptions $buildingOptions = null
        ): \Uri\QueryParams {}
 
        /**
         * @throws \Uri\InvalidQueryParamsException
         */
        public static function parseRfc3986(
            string $queryString,
            ?\Uri\QueryParamParsingOptions $parsingOptions = null,
            ?\Uri\QueryParamBuildingOptions $buildingOptions = null
        ): \Uri\QueryParams {}
 
        /**
         * @throws \Uri\InvalidQueryParamsException
         */
        public static function parseWhatWg(
            string $queryString,
            ?\Uri\QueryParamParsingOptions $parsingOptions = null,
            ?\Uri\QueryParamBuildingOptions $buildingOptions = null
        ): \Uri\QueryParams {}
 
        /**
         * @throws \ValueError
         */
        public static function fromArray(
            array $queryParams,
            ?\Uri\QueryParamBuildingOptions $buildingOptions = null
        ): \Uri\QueryParams {}
 
        public function __construct(?\Uri\QueryParamBuildingOptions $buildingOptions = null) {}
 
        public function has(string $name): bool {}
 
        /**
         * @throws \TypeError
         */
        public function hasValue(string $name, mixed $value): bool {}
 
        public function getFirst(string $name): ?string {}
 
        public function getLast(string $name): ?string {}
 
        public function getAll(string $name): array {}
 
        public function list(): array {}
 
        public function getIterator(): \Traversable {}
 
        public function count(): int {}
 
        /**
         * @throws \TypeError
         */
        #[\NoDiscard]
        public function append(string $name, mixed $value): static {}
 
        /**
         * @throws \TypeError
         */
        #[\NoDiscard]
        public function set(string $name, mixed $value): static {}
 
        #[\NoDiscard]
        public function delete(string $name): static {}
 
        /**
         * @throws \TypeError
         */
        #[\NoDiscard]
        public function deleteValue(string $name, mixed $value): static {}
 
        #[\NoDiscard]
        public function sort(): static {}
 
        /**
         * @throws \TypeError
         */
        public function withArray(array $queryParams): static {}
 
        /**
         * @throws \Uri\InvalidQueryParamsException
         * @throws \ValueError
         */
        public function toArray(int $maxNestingLevel = 64): array {}
 
        public function toRfc1866String(): string {}
 
        public function toRfc3986String(): string {}
 
        public function toWhatWgString(): string {}
 
        public function __serialize(): array {}
 
        public function __unserialize(array $data): void {}
 
        public function __debugInfo(): array {}
    }
}
namespace Uri\Rfc3986 {
    final readonly class Uri
    {
        // ...
 
        /**
         * @throws \Uri\InvalidQueryParamsException
         */
        public function getQueryParams(
            ?\Uri\QueryParamParsingOptions $parsingOptions = null,
            ?\Uri\QueryParamBuildingOptions $buildingOptions = null
        ): ?\Uri\QueryParams {}
 
        public function withQueryParams(?\Uri\QueryParams $queryParams): static {}
 
        // ...
    }
}
 
namespace Uri\WhatWg {
    final readonly class Url
    {
        // ...
 
        /**
         * @throws \Uri\InvalidQueryParamsException
         */
        public function getQueryParams(
            ?\Uri\QueryParamParsingOptions $parsingOptions = null,
            ?\Uri\QueryParamBuildingOptions $buildingOptions = null
        ): ?\Uri\QueryParams {}
 
        public function withQueryParams(?\Uri\QueryParams $queryParams): static {}
 
        // ...
    }
}

Construction & parsing

QueryParams supports the following methods for instantiation:

  • Uri\QueryParams::parseRfc1866(): It parses a query string into a list of query parameters according to the processing and percent-decoding rules of the application/x-www-form-urlencoded media type, as defined by RFC 1866. This specification regards query parameters as a list of name-value pairs, where the two parts are separated by a “=” character, and the individual parameters are separated from each other by a “&” character (e.g. name1=value1&name2=value2).
  • Uri\QueryParams::parseRfc3986(): It parses a query string into a list of query parameters according to the percent-decoding rules of RFC 3986, with the caveat that this specification in fact does not specify exactly how query parameters are composed. That's why the implementation defines query parameters based on the definition of RFC 1866.
  • Uri\QueryParams::parseWhatWg(): It parses a query string into a list of query parameters according to the percent-decoding rules of the application/x-www-form-urlencoded media type, as defined by the WHATWG URL specification.
  • Uri\QueryParams::fromArray(): It takes an array of query parameters and directly composes the query parameter list object based on it. Besides scalar values, it can also accept complex types such as arrays according to the rules discussed in the "Array API" section.
  • Uri\QueryParams::__construct(): It creates an empty query parameter list and optionally accepts building options. This method allows building query parameters from scratch.
  • Uri\Rfc3986\Uri::getQueryParams(): It creates a new Uri\QueryParams instance by parsing the query string component stored by Uri\Rfc3986\Uri.
  • Uri\WhatWg\Url::getQueryParams(): It creates a new Uri\QueryParams instance by parsing the query string component stored by Uri\WhatWg\Url.
$params = Uri\QueryParams::parseRfc1866("a=foo&b=bar");  // Successful instantiation
$params = Uri\QueryParams::parseRfc3986("a=foo&b=bar");  // Successful instantiation
$params = Uri\QueryParams::parseWhatWg("a=foo&b=bar");   // Successful instantiation
$params = Uri\QueryParams::fromArray(
    [
        "a" => "foo",
        "b" => "bar",
    ]
);                                                       // Successful instantiation - same result as above
$params = new Uri\QueryParams();                         // Successful instantiation - creates an empty query parameter list

It's important to highlight that QueryParams does not require the input to be a syntactically valid URI query component during construction. This behavior is by design, because the idea of WHATWG URL's URLSearchParams class is that it's tolerant for reading, and QueryParams follows the same principle. Validation happens anyway when the recomposed query parameters are attempted to be written to a URI (via Uri\Rfc3986\Uri::withQuery() and Uri\WhatWg\Url::withQuery()). However, as we'll see, invalid characters are automatically percent-encoded during query parameter recomposition, so the withQuery() calls won't fail in practice either.

$params = Uri\QueryParams::parseRfc3986("#foo=bar");     // Parses an invalid parameter name "#foo"
 
$uri = new Uri\Rfc3986\Uri("https://example.com/");
 
$uri = $uri->withQuery($params->toRfc3986String());      // Success: the query is automatically percent-encoded to "%23foo=bar"

The same example for WHATWG URL:

$params = Uri\QueryParams::parseWhatWg("#foo=bar");      // Parses an invalid parameter name "#foo"
 
$url = new Uri\WhatWg\Url("https://example.com/");
 
$url = $url->withQuery($params->toWhatWgString());       // Success: the query is automatically percent-encoded to "%23foo=bar"

All parse*() variants percent-decode the input automatically when constructing the QueryParams instance. This is necessary so that one can work with the unencoded query parameters.

$params = Uri\QueryParams::parseRfc1866("foo%5B%5D=b%61r");  // Percent-encoded form of "foo[]=bar"
// $params internally contains the ["foo[]" => "bar"] key-value pair
 
$params = Uri\QueryParams::parseRfc3986("foo%5B%5D=b%61r");  // Percent-encoded form of "foo[]=bar"
// $params internally contains the ["foo[]" => "bar"] key-value pair
 
$params = Uri\QueryParams::parseWhatWg("foo%5B%5D=b%61r");   // Percent-encoded form of "foo[]=bar"
// $params internally contains the ["foo[]" => "bar"] key-value pair

Uri\QueryParams::parseRfc1866() and Uri\QueryParams::parseRfc3986() split the non-empty input query string at each literal & character before percent-decoding its fields. Each field produces a parameter, including empty fields before a leading &, after a trailing &, or between consecutive & characters.

The first literal = in each field separates the parameter name from its value. If there is no =, the value is null. If = is present but nothing follows it, the value is an empty string. Additional = characters belong to the value.

$params = Uri\QueryParams::parseRfc3986("foo=bar&");
$list = $params->list();                                // [["foo", "bar"], ["", null]]
 
echo $params->toRfc3986String();                        // foo=bar&
$params = Uri\QueryParams::parseRfc3986("foo=bar&=");
$list = $params->list();                                // [["foo", "bar"], ["", ""]]
 
echo $params->toRfc3986String();                        // foo=bar&=

An empty input query string (“”) produces an empty parameter list. By comparison, & represents two parameters with empty names and null values, while = represents one parameter with an empty name and an empty string value.

An empty list and a list containing only one parameter with an empty name and a null value both recompose to an empty string. Consequently, recomposing and reparsing a parameter list is not guaranteed to reproduce every possible list.

The empty-field and missing-value rules above are conventions of this API, shared by parseRfc1866() and parseRfc3986().

In contrast, parseWhatWg() discards empty fields in line with the WHATWG URL specification. A field without an = character has an empty string value rather than null. An empty name is allowed, and only the first literal = separates the name from the value.

Input Result of parseWhatWg()->list()
“”, “&”, or “&&” []
“foo” or “foo=” [["foo", ""]]
“=” [["", ""]]
“=bar” [["", "bar"]]
“foo=bar&” [["foo", "bar"]]
“foo=bar&=” [["foo", "bar"], ["", ""]]
“foo=a=b” [["foo", "a=b"]]

The tolerant parsing principle is also applied when encountering malformed percent-encoded sequences, such as %, %2, or %GG. QueryParams::parseRfc1866() and QueryParams::parseRfc3986() preserve these literally both in parameter names and values rather than throwing an exception. This behavior is a design decision on PHP's side, as there's no relevant mandate either in RFC 1866 or in RFC 3986.

$params = Uri\QueryParams::parseRfc3986("foo=%&bar=%2&baz=%GG");
 
var_dump($params->getFirst("foo"));        // string(1) "%"
var_dump($params->getFirst("bar"));        // string(2) "%2"
var_dump($params->getFirst("baz"));        // string(3) "%GG"
 
echo $params->toRfc3986String();           // foo=%25&bar=%252&baz=%25GG

On the other hand, WHATWG URL has detailed requirements for handling malformed percent-encoded sequences that QueryParams::parseWhatWg() respects: these sequences are also preserved during parsing.

$params = Uri\QueryParams::parseWhatWg("foo=%&bar=%2&baz=%GG");
 
var_dump($params->getFirst("foo"));        // string(1) "%"
var_dump($params->getFirst("bar"));        // string(2) "%2"
var_dump($params->getFirst("baz"));        // string(3) "%GG"
 
echo $params->toWhatWgString();            // foo=%25&bar=%252&baz=%25GG

When recomposing the query string, literal percent signs are encoded as %25. Consequently, the parameter values are preserved, but the original query string is not necessarily reproduced.

Valid percent-encoded sequences are decoded into bytes. parseRfc1866() and parseRfc3986() preserve these bytes without character-encoding conversion or UTF-8 validation. For example, %FF represents the byte 0xFF, even though that byte alone is not valid UTF-8:

$params = Uri\QueryParams::parseRfc3986("foo=%FF&bar=%C3%A9");
 
echo bin2hex($params->getFirst("foo"));    // ff
var_dump($params->getFirst("bar"));        // string(2) "é"
 
echo $params->toRfc3986String();           // foo=%FF&bar=%C3%A9

In contrast, parseWhatWg() applies UTF-8 decoding after percent-decoding based on the requirements of WHATWG URL. Invalid UTF-8 sequences are replaced with U+FFFD (the replacement character), while valid UTF-8 is preserved:

$params = Uri\QueryParams::parseWhatWg("foo=%FF&bar=%C3%A9");
 
var_dump($params->getFirst("foo"));        // string(3) "�"
echo bin2hex($params->getFirst("foo"));    // efbfbd
var_dump($params->getFirst("bar"));        // string(2) "é"
 
echo $params->toWhatWgString();            // foo=%EF%BF%BD&bar=%C3%A9

This replacement loses the original invalid bytes: recomposing the query string encodes the replacement character rather than recovering the original %FF.

According to WHATWG URL's new URLSearchParams(init) algorithm, exactly one leading “?” character is removed during the QueryParams::parseWhatWg() call. In contrast to this behavior, the leading “?” becomes part of the first query parameter name for RFC 1866 and RFC 3986 query params.

$params = Uri\QueryParams::parseRfc3986("?abc=foo");
// $params internally contains the ["?abc" => "foo"] key-value pair
 
$params = Uri\QueryParams::parseWhatWg("?abc=foo");
// $params internally contains the ["abc" => "foo"] key-value pair

All parse*() methods accept an optional QueryParamParsingOptions instance. If it is omitted or null, the default limits apply:

  • $maxQueryStringLength limits the length of the input query string in bytes and defaults to 10000. The length is measured before percent-decoding or any other parsing step, including removal of a leading ? by parseWhatWg(). A value of 0 permits only an empty input string.
  • $maxParamCount limits the number of parsed name-value pairs and defaults to 1000, similar to the max_input_vars INI option. Each occurrence counts separately, including duplicate names and parameters without a value. A value of 0 permits only inputs that produce no parameters.

Both limits are inclusive. If either limit is exceeded, the parsing method throws an Uri\InvalidQueryParamsException and does not return a partially populated instance. Negative limits cause the QueryParamParsingOptions constructor to throw a ValueError.

These limits apply only when parsing a query string. They do not restrict fromArray(), withArray(), or subsequent calls to append() and set().

$options = new Uri\QueryParamParsingOptions(
    maxQueryStringLength: 11,
    maxParamCount: 2,
);
 
$params = Uri\QueryParams::parseRfc3986("a=1&a=2", parsingOptions: $options);   // Two parameters: accepted
 
Uri\QueryParams::parseRfc3986("a=1&a=2&a=3", parsingOptions: $options);         // Throws Uri\InvalidQueryParamsException
Uri\QueryParams::parseRfc3986("a=1&a=234567", parsingOptions: $options);        // Throws Uri\InvalidQueryParamsException

Parameter Retrieval

The has() and hasValue() methods can be used to find out if a parameter exists:

$params = Uri\QueryParams::parseRfc3986("foo=bar&baz=qux&baz=quux");
 
var_dump($params->has("baz"));              // bool(true)
var_dump($params->has("non-existent"));     // bool(false)
 
var_dump($params->hasValue("foo", "bar"));  // bool(true) 
var_dump($params->hasValue("foo", "baz"));  // bool(false)

The has() method returns true if there is at least one parameter in the parameter list with the given name, false otherwise. On the other hand, hasValue() returns true if the given name and value both match at least one parameter, otherwise it returns false.

The number of query parameters can be retrieved by calling the count() method:

$params = Uri\QueryParams::parseRfc3986("foo=bar&baz=qux&baz=quux");
 
echo $params->count();                    // 3

There are also a number of methods that can return a query parameter or an array of query parameters:

  • getFirst(): Retrieves the first parameter with the given name. This actually implements the get() method from the WHATWG URL specification.
  • getLast(): Retrieves the last parameter with the given name. It's a custom, PHP-specific method which doesn't have a WHATWG URL equivalent.
  • getAll(): Retrieves all parameters with the given name. This actually implements the getAll() method from the WHATWG URL specification.
  • list(): Retrieves all query parameters. It's also a custom, PHP-specific method which doesn't have a WHATWG URL equivalent.
$params = Uri\QueryParams::parseRfc3986("foo=bar&foo=baz&qux=quux");
 
var_dump($params->getFirst("foo"));          // string(3) "bar"
var_dump($params->getFirst("non-existent")); // NULL
 
var_dump($params->getLast("foo"));           // string(3) "baz"
var_dump($params->getLast("non-existent"));  // NULL
 
$all = $params->getAll("foo");               // ["bar", "baz"]
$all = $params->getAll("non-existent");      // []
 
$list = $params->list();                     // [["foo", "bar"], ["foo", "baz"], ["qux", "quux"]]

All these methods return the natively stored values without applying any transformations. That is, percent-encoding or decoding neither happens in the input, nor in the output.

$params = Uri\QueryParams::parseRfc3986("foo%5B%5D=b%61r");  // Internally stored as "foo[]=bar"
 
var_dump($params->getFirst("foo%5B%5D"));    // NULL
var_dump($params->getFirst("foo[]"));        // string(3) "bar"
 
var_dump($params->getLast("foo%5B%5D"));     // NULL
var_dump($params->getLast("foo[]"));         // string(3) "bar"
 
$all = $params->getAll("foo%5B%5D");         // []
$all = $params->getAll("foo[]");             // ["bar"]
 
$list = $params->list();                     // [["foo[]", "bar"]]

Percent-Encoding and Decoding

QueryParams only performs percent-encoding when query parameters are recomposed to a query string (via to*String() methods), and it only performs percent-decoding when a query string is parsed into a query parameter list (via parse*() methods). The rest of the functionalities don't use percent-encoding or decoding.

QueryParams supports percent-encoding and decoding according to three specifications:

  • RFC 1866 which specifies the percent-encoding and decoding rules of the application/x-www-form-urlencoded media type
  • RFC 3986 which defines the generic query string syntax.
  • URLSearchParams class specified by WHATWG URL, which yet again builds upon the application/x-www-form-urlencoded media type for historic reasons, albeit slightly differently than how RFC 1866 specifies it.

The current section is going to have an overview about the percent-encoding and decoding details, as well as the differences between the aforementioned specifications.

According to RFC 1866, space characters are replaced by the plus character (+) during percent-encoding, and any characters that fall outside of the unreserved character set are percent-encoded. Percent-decoding inverts these operations.

This behavior clearly deviates from the percent-encoding rules of the query component of RFC 3986 which allows quite a few reserved characters to be present in the query component without percent-encoding (a few examples: “:”, “@”, “?”, “/”), not to mention the difference in how the space character is handled.

Regarding WHATWG URL's URLSearchParams class, as usual, a dedicated percent-encoding set is defined:

The application/x-www-form-urlencoded percent-encode set contains all code points, except the ASCII alphanumeric, U+002A (*), U+002D (-), U+002E (.), and U+005F (_).

WHATWG URL also defines a dedicated algorithm for “serialization” (in this context, serialization means recomposition - converting the list to a query string): the space code point is percent-encoded as the plus code point (+), and the rest of the code points in the percent-encoding set are encoded how WHATWG URL normally does so.

This behavior deviates from the percent-encoding rules of the query component of WHATWG URL, as the query percent-encode set contains far fewer characters, and the space code point is handled differently again.

It's also important to compare how the percent encoding rules of RFC 1866's as well as WHATWG URL's application/x-www-form-urlencoded media type differ: RFC 1866 percent-encodes the asterisk (*) symbol as %2A, while WHATWG URL leaves it unescaped.

Recomposition

In order to be consistent with the design of Uri\Rfc3986\Uri and the Uri\WhatWg\Url classes, QueryParams doesn't have a __toString() magic method, especially because it offers multiple possibilities for recomposition. Instead, it contains custom to*String() methods to recompose the query string from the parsed query parameters according to the supported formats.

$params = Uri\QueryParams::parseRfc3986("foo=bar&foo=baz");
echo $params->toRfc1866String();          // foo=bar&foo=baz
echo $params->toRfc3986String();          // foo=bar&foo=baz
echo $params->toWhatWgString();           // foo=bar&foo=baz

All to*String() methods (Uri\QueryParams::toRfc1866String(), Uri\QueryParams::toRfc3986String(), Uri\QueryParams::toWhatWgString()) automatically percent-encode their output according to the rules outlined in the previous section, otherwise it would be possible that an invalid output is returned.

$params = Uri\QueryParams::fromArray(["foo[]" => "bar baz"]);
echo $params->toRfc1866String();         // foo%5B%5D=bar+baz
echo $params->toRfc3986String();         // foo%5B%5D=bar%20baz
echo $params->toWhatWgString();          // foo%5B%5D=bar+baz

Unlike Uri\Rfc3986\Uri, the Uri\QueryParams class doesn't have a toRawString() method because it could be misleading what it exactly does: toRawString() cannot really provide a “raw” representation of the query string, since automatic percent-encoding must happen anyway to make the produced query string valid.

Relation to Uri\Rfc3986\Uri and Uri\WhatWg\Url

It should be clarified how the new class can interoperate with the query component of the existing Uri\Rfc3986\Uri and Uri\WhatWg\Url classes.

Both Uri\Rfc3986\Uri and Uri\WhatWg\Url provide getQueryParams() and withQueryParams() methods for retrieving and replacing their query parameters.

Uri\Rfc3986\Uri::getQueryParams() parses the raw (unnormalized) query component using the RFC 3986-based rules, and Uri\WhatWg\Url::getQueryParams() follows the WHATWG URL specification. If the query component is absent, they both return null. Otherwise, each call creates a new QueryParams instance, including an empty instance when the query component is present but empty. Returning null for a missing query component ensures that passing the result back to withQueryParams() does not introduce an empty query component.

$uri = new Uri\Rfc3986\Uri("https://example.com");
 
$params = $uri->getQueryParams();                     // null
 
$uri = $uri->withQueryParams($params);
 
var_dump($uri->getQuery());                           // NULL

Both getQueryParams() methods accept optional $parsingOptions and $buildingOptions arguments, with the same defaults as the corresponding QueryParams::parse*() methods. Exceeding a parsing limit throws an Uri\InvalidQueryParamsException. Building options configure subsequent modifications of the returned instance.

For WHATWG URLs, a distinction must be made between parsing a query component and invoking the URLSearchParams constructor with a string. Url::getQuery() returns the component without its delimiting ?, so any leading ? in that result belongs to the first query parameter itself. Passing this value directly to QueryParams::parseWhatWg() would remove that character. Url::getQueryParams() handles this distinction by parsing the component directly, without removing a leading ?:

$url = new Uri\WhatWg\Url("https://example.com/??foo=bar");
 
echo $url->getQuery();                                // ?foo=bar
 
// Incorrect usage:
$params = Uri\QueryParams::parseWhatWg($url->getQuery());
$list = $params->list();                              // [["foo", "bar"]]
 
// Correct usage:
$params = $url->getQueryParams();
$list = $params->list();                              // [["?foo", "bar"]]
 
// Also correct:
$params = Uri\QueryParams::parseWhatWg("?" . $url->getQuery());
$list = $params->list();                              // [["?foo", "bar"]]

withQueryParams() creates a new URI or URL with its entire query component replaced. It uses toRfc3986String() for Uri\Rfc3986\Uri and toWhatWgString() for Uri\WhatWg\Url. Passing null removes the query component.

$url = new Uri\WhatWg\Url("https://example.com/?page=1");
 
$params = $url
    ->getQueryParams()
    ->set("page", 2);
 
$url = $url->withQueryParams($params);
 
echo $url->getQuery();                                // page=2

An empty recomposed query string follows the behavior of the existing withQuery(“”) method: Uri\Rfc3986\Uri retains an empty query string component, while Uri\WhatWg\Url removes the component. Consequently, the RFC-based methods preserve the distinction between an absent and an empty query component when reading and writing an empty query string back.

Although preserving the original query string where possible was an important design goal, parsing and recomposing cannot always reproduce the original query string. QueryParams stores decoded parameter names and values, so the original percent-encoded representation is not retained. For example, x=/ and x=%2F both produce the same parameter list, containing the name “x” and the value “/”, and recomposition cannot distinguish between these original representations.

Moreover, parsing and recomposing a WHATWG URL query string does not necessarily preserve its original string representation. The WHATWG URL specification defines different percent-encoding rules for URL query components and application/x-www-form-urlencoded data. For example, a space represented as %20 in the original URL becomes + when recomposed by toWhatWgString():

$url = new Uri\WhatWg\Url("https://example.com?foo=a%20b");   // "a%20b" is the percent-encoded form of "a b"
 
$params = $url->getQueryParams();
 
$url = $url->withQueryParams($params);
 
$array = $params->toArray();                                  // ["foo" => "a b"]
echo $url->getQuery();                                        // foo=a+b

In this example, the parameter name and its internally stored (percent-decoded) value remain unchanged, even though the query string has a different representation.

Modification

The append() method can be used to append a parameter to the end of the list. As usual, the same query parameter can be added multiple times:

$params = Uri\QueryParams::parseRfc3986("foo=bar");
$params = $params->append("baz", "qux");
$params = $params->append("baz", "qaz");             // Appends "baz" twice
 
echo $params->toRfc3986String();                     // foo=bar&baz=qux&baz=qaz

Updating a parameter is possible via the set() method:

$params = Uri\QueryParams::parseRfc3986("foo=bar&foo=baz");
$params = $params->set("foo", "baz");                // Overwrites the first item "foo", and removes the second one
$params = $params->set("qux", "qaz");                // Appends a new item "qux"
 
echo $params->toRfc3986String();                     // foo=baz&qux=qaz

Actually, the set() method has a hybrid behavior: if a parameter is not present in the list, then it adds it just like append() does. Otherwise, it overwrites the first item, and removes the rest of the occurrences.

Neither append() nor set() performs percent-encoding or decoding on its arguments, and the input is stored literally. However, as mentioned in the section about Recomposition, each literal “%” character is encoded as “%25”, when the query string is recomposed:

$params = new Uri\QueryParams();
$params = $params->append("foo%5B%5D", "ab%63");
$params = $params->set("bar%5B%5D", "de%66");
 
echo $params->getFirst("foo%5B%5D");    // ab%63
echo $params->getFirst("bar%5B%5D");    // de%66
 
echo $params->toRfc3986String();        // foo%255B%255D=ab%2563&bar%255B%255D=de%2566

Therefore, callers should pass decoded names and values to these methods. For example, $params->append(“foo[]”, “abc”) produces the expected “foo%5B%5D=abc” query string when recomposed.

append() and set() also preserve the bytes of string parameter names and values without UTF-8 validation or replacement. Invalid UTF-8 sequences remain in the parameter list: RFC 1866 and RFC 3986-based recomposition methods percent-encode their original bytes, while toWhatWgString() replaces them with U+FFFD when producing its output, leaving the stored data unchanged.

Removing parameters is possible via either the delete() or the deleteValue() method: the former one removes all occurrences of the given parameter name, while the latter one removes all occurrences of a parameter if the given name and value both match it, as demonstrated below:

$params = Uri\QueryParams::parseRfc3986("foo=bar&foo=baz&foo=qux");
$params = $params->deleteValue("foo", "baz");        // Deletes the "foo=baz" parameter
$params = $params->delete("foo");                    // Deletes the rest of the occurrences: "foo=bar" and "foo=qux"
$params = $params->delete("non-existent");           // The parameter is not present: nothing happens

Finally, sort() sorts the query parameter list by parameter name in ascending lexicographical order, comparing byte values. The comparison is case-sensitive and locale-independent:

$params = Uri\QueryParams::parseRfc3986("foo=bar&baz=qux&baz=quux");
$params = $params->sort();
 
echo $params->toRfc3986String();                     // baz=qux&baz=quux&foo=bar

Parameters with the same name (baz in the above example) keep their original order rather than being ordered by value. This is necessary because parameters with the same name usually refer to lists, and modifying the order of list items would definitely change the original meaning.

This differs from WHATWG URL's URLSearchParams::sort(), which compares UTF-16 code units. Consequently, the two methods can produce different orders for non-ASCII names.

The main purpose of sort() is to provide a consistent order of the key-value pairs (e.g. to increase cache hits), therefore more advanced features such as sorting in descending order, or user-provided comparison methods are not proposed.

Supported types

What's also important to clarify is how non-string values are mapped to query parameters which inherently have string type? PHP's http_build_query() can basically map any type to query parameters, however, this is purely a PHP-specific behavior, and as such, type mapping rules are out of scope of both RFC 3986 and WHATWG URL: RFC 3986 completely omits any information how query parameters should be built, while WHATWG URL's URLSearchParams constructor only accepts and returns string data according to the specification itself.

The position of this RFC is that it's important to follow the road that http_build_query() has already paved because of better developer experience and better interoperability with the existing ecosystem. That's why the following type mapping behavior is proposed when a query parameter is added/updated:

  • null: Omits the query parameter value by default
  • bool: Becomes string “0” (in case of false) or string “1” (in case of true) by default
  • int: Becomes a numeric string (123 -> “123”)
  • float: Converted according to PHP's regular float-to-string conversion rules (e.g. 3.14 -> “3.14”)
  • resource: Invalid mapping, a TypeError exception is thrown
  • array: Invalid mapping, a TypeError exception is thrown
  • enum:
    • backed enums are converted to their backing value
    • enums without backing type are invalid, a TypeError exception is thrown
  • object: invalid mapping, a TypeError exception is thrown

A few examples demonstrating how QueryParams handles non-string scalar types:

$params = new Uri\QueryParams();
 
$params = $params->append("null", null);
$params = $params->append("bool", true);
$params = $params->append("int", 123);
$params = $params->append("float", 3.14);
 
var_dump($params->getFirst("null"));        // NULL
var_dump($params->getFirst("bool"));        // string(1) "1"
var_dump($params->getFirst("int"));         // string(3) "123"
var_dump($params->getFirst("float"));       // string(4) "3.14"
 
echo $params->toRfc3986String();            // null&bool=1&int=123&float=3.14

The default mapping of boolean and null values can be customized during query parameter building (fromArray(), withArray(), append() and set()) by passing a Uri\QueryParamBuildingOptions instance to the QueryParams constructor or its factory methods. The following properties control these conversions:

  • $trueValue: The string representation of true. Defaults to “1”.
  • $falseValue: The string representation of false. Defaults to “0”.
  • $nullPolicy: How query parameters with a null value are treated.
    • \Uri\QueryParamNullPolicy::SkipTuple: The parameter is not added to the query parameter list.
    • \Uri\QueryParamNullPolicy::KeyOnly: The parameter name is added, but the value is omitted. This is the default behavior.
    • \Uri\QueryParamNullPolicy::EmptyString: The parameter value is converted to an empty string.

For example, $trueValue can be used to represent enabled flags as “yes”:

$options = new Uri\QueryParamBuildingOptions(trueValue: "yes");
$params = new Uri\QueryParams($options);
$params = $params->append("enabled", true);
 
var_dump($params->getFirst("enabled"));     // string(3) "yes"
echo $params->toRfc3986String();            // enabled=yes

Similarly, $falseValue controls how disabled flags are represented:

$options = new Uri\QueryParamBuildingOptions(falseValue: "no");
$params = new Uri\QueryParams($options);
$params = $params->append("enabled", false);
 
var_dump($params->getFirst("enabled"));     // string(2) "no"
echo $params->toRfc3986String();            // enabled=no

By setting $nullPolicy to \Uri\QueryParamNullPolicy::KeyOnly, the query parameter name is stored, but its value is omitted:

$options = new Uri\QueryParamBuildingOptions(nullPolicy: \Uri\QueryParamNullPolicy::KeyOnly);
$params = new Uri\QueryParams($options);
$params = $params->append("param", null);
 
var_dump($params->getFirst("param"));       // NULL
var_dump($params->toRfc3986String());       // string(5) "param"

On the other hand, setting $nullPolicy to \Uri\QueryParamNullPolicy::EmptyString removes the distinction between a null value and an empty string:

$options = new Uri\QueryParamBuildingOptions(nullPolicy: \Uri\QueryParamNullPolicy::EmptyString);
$params = new Uri\QueryParams($options);
$params = $params->append("param", null);
 
var_dump($params->getFirst("param"));       // string(0) ""
var_dump($params->toRfc3986String());       // string(6) "param="

For reference, http_build_query() omits parameters with null values (thus conforms to the behavior of \Uri\QueryParamNullPolicy::SkipTuple):

$options = new Uri\QueryParamBuildingOptions(nullPolicy: \Uri\QueryParamNullPolicy::SkipTuple);
$params = new Uri\QueryParams($options);
$params = $params->append("param", null);
 
var_dump($params->has("param"));            // bool(false)
var_dump($params->toRfc3986String());       // string(0) ""

Type conversions listed above happen when query parameter values are checked (hasValue()), added (append()), updated (set()), or deleted (deleteValue()), rather than when the query string is recomposed. The building options are stored by QueryParams instances, so subsequent query parameter modification operations use the same mapping rules. The configured strings are treated as ordinary parameter values: they are stored without percent-encoding and encoded when a query string is produced.

enum Flag
{
    case Enabled;
}
 
$params = new Uri\QueryParams();
 
$params = $params->append("value", fopen("php://memory", "r+"));    // Throws TypeError
$params = $params->append("value", ["foo", "bar"]);                 // Throws TypeError
$params = $params->append("value", Flag::Enabled);                  // Throws TypeError

hasValue() and deleteValue() convert non-null values using the same type mapping rules as append(), including the configured boolean representations. For these two methods, the $nullPolicy option is ignored when comparing a null argument against the stored query parameter values, while an empty string matches only an empty string query parameter value. Names and converted string values are compared byte-for-byte.

To use the string representation of a Stringable object, callers must explicitly cast it to a string before passing it as a parameter value. Backed enums do not require an explicit conversion: their backing value is mapped according to the rules for strings or integers.

Rejecting arrays as individual parameter values does not prevent array-based construction. fromArray() and withArray() provide a separate API for converting PHP arrays into parameter lists, as described in the following section.

Building options do not affect parsing. Options passed to a parsing method are stored for subsequent operations. The configured $nullPolicy applies when building parameters through fromArray(), withArray(), append(), or set(), while boolean representations ($trueValue, $falseValue) also apply to hasValue() and deleteValue() in addition to the previously mentioned methods.

Array API

The original proposal included a dedicated set of methods for working with PHP-style bracket-syntax arrays directly on the QueryParams object (getArray(), appendArray(), setArray()). After implementation experience and discussion, this approach was abandoned in favor of a significantly simpler design.

The core problem with the original approach was that it required QueryParams to reimplement PHP's native array operations internally. For example, QueryParams::getArray(“foo[bar][baz]”) would have needed to parse the array dimensions at runtime and navigate a nested structure, and appendArray() / setArray() would have required equivalent complexity for writes. Beyond the implementation burden, the resulting API surface was too large, the method names were confusable (e.g. getArray()vs. getAll()), and the behavior around bracket parsing in method arguments was difficult to define precisely and consistently.

The simplified Array API consists of three methods:

  • QueryParams::fromArray(): Creates a new QueryParams instance from a PHP array, using the bracket-syntax encoding rules described in this section to convert nested arrays into flat key-value pairs.
  • QueryParams::toArray(): Converts the parameter list into a PHP array by interpreting bracket notation in parameter names according to the rules described below.
  • QueryParams::withArray(): It replaces the whole query list with the input, but clones the current instance rather than creating a completely new one.

The intended workflow for array-based modifications is:

$params = Uri\QueryParams::parseRfc3986("page=1&debug=false");
 
// Convert to a native PHP array, modify using native PHP operations
// then convert back to a QueryParams instance.
$array = $params->toArray();
$array["page"] = 2;
unset($array["debug"]);
 
$params = $params->withArray($array);
 
// Or
 
$params = Uri\QueryParams::fromArray($array);

This design delegates array manipulation to PHP's native array functions rather than reimplementing them inside QueryParams. The result is a smaller API surface, predictable behavior, and better composability with existing PHP code.

The Array API distinguishes the following array types:

  • Empty array: An array containing no elements.
  • List: An array whose keys are consecutive integers starting from 0, in iteration order, as determined by array_is_list().
  • Map: A non-empty array is a map if it is not a list.

fromArray() and withArray() convert PHP arrays into parameter lists according to the following rules:

  • The outermost input array contains the query parameters to build. The keys will become parameter names: when encountering integer keys, int to string conversion is performed. Array items are processed in the regular array iteration order.
  • Non-array values are converted according to the rules described in the “Supported types” section, using the present building options.
  • Empty arrays are omitted, since they contain no items which could be added to the query parameter list.
  • For a nested list containing no array elements, [] is appended to the resulting query parameter name for each element. For example, the [“k” => [1, 2]] input is internally stored as two parameters: [["k[]" => "1"], ["k[]" => "2"]].
  • If a nested list contains at least one array element, the original numeric index is included for every element at that level. For example, the [“k” => [1, [“a” => 2]]] input is internally stored as [["k[0]" => "1"], ["k[1][a]" => "2"]].
  • For a nested map, each key is appended to the parameter name within brackets. For example, the [“k” => [“a” => 1, “b” => 2]] input is internally stored as [["k[a]" => "1"], ["k[b]" => "2"]].
  • These rules apply recursively.

The fromArray() and withArray() methods reject recursive array structures with a ValueError:

$array = [];
$array["self"] = &$array;
 
Uri\QueryParams::fromArray($array);         // Throws ValueError

References are otherwise supported. Referencing the same non-recursive array from multiple independent branches is allowed, and each occurrence is converted separately.

The toArray() method converts the stored parameter list into a PHP array. It interprets bracket-syntax in parameter names to reconstruct nested arrays. The following rules apply:

  • Empty parameter list: Produces an empty PHP array.

  • Names without bracket notation: A single query parameter name occurrence produces a scalar value. Repeated occurrences are collected into a list of values in encounter order. For example, k=1 produces [“k” => “1”], while k=1&k=2 produces [“k” => [“1”, “2”]].

  • Empty brackets: Each [] appends a new element to the resulting array list using PHP's normal array appending semantics. Even a single occurrence produces an array: k[]=1 produces [“k” => [“1”]]. If further bracket segments follow, a new nested array is created for that occurrence. For example, k[][a]=1&k[][b]=2 creates two separate items within the list k: ["k" => [["a" => "1"], ["b" => "2"]]].

  • Explicit keys: A non-empty bracket segment selects the corresponding array key. Distinct paths may share intermediate arrays, so k[a]=1&k[b]=2 creates one array containing both keys. Assigning another value to an already populated leaf, as in k[a]=1&k[a]=2, throws an Uri\InvalidQueryParamsException.

  • Array key conversion: Root names and explicit bracket keys follow PHP's normal array key conversion rules. For example, “1” becomes an integer key, while “01” remains a string key.

  • Conflicting structures: Plain parameter names and bracket paths must not be mixed for the same root, as in k=1&k[]=2. Scalar/array conflicts at deeper levels, such as k[a]=1&k[a][b]=2, are also rejected with an Uri\InvalidQueryParamsException, regardless of parameter order.

  • Malformed bracket notation: The entire parameter name is treated literally, without partial bracket interpretation. Examples include k[, foo[]], and foo[[a]. Repeated occurrences of these literal names follow the same collection rule as other names without interpreted bracket notation.

  • Values: Stored string and null values are preserved. Building options are not reapplied during conversion.

Explicit indices are necessary when list elements contain arrays, because empty brackets would otherwise lose the grouping between fields belonging to the same element.

$params = Uri\QueryParams::fromArray([
    "rows" => [
        ["a" => 1, "b" => 2],
        ["a" => 3, "b" => 4],
    ],
]);
 
$list = $params->list();
// [
//     ["rows[0][a]", "1"],
//     ["rows[0][b]", "2"],
//     ["rows[1][a]", "3"],
//     ["rows[1][b]", "4"],
// ]
 
$array = $params->toArray();
// [
//     "rows" => [
//         ["a" => "1", "b" => "2"],
//         ["a" => "3", "b" => "4"],
//     ],
// ]

toArray() is not a general inverse of fromArray(). Values undergo the conversions described in the “Supported types” section, empty arrays are omitted, and bracket notation in parameter names is interpreted when constructing the result. When explicit indices are used, omitted elements may leave gaps in the resulting array's numeric keys.

The toArray() method accepts a $maxNestingLevel parameter, which limits the nesting depth of the resulting PHP array. It defaults to 64. The outermost array is not counted as a nesting level: a=1 has a nesting level of 0, a[b]=1 has a nesting level of 1, and a[b][c]=1 has a nesting level of 2. Each additional nested array increases the nesting level by one.

If converting the parameter list would exceed this limit, toArray() throws an Uri\InvalidQueryParamsException. Passing 0 only permits a flat result array. Negative values cause a ValueError to be thrown.

This limit only applies when converting query parameters to a PHP array. Parameter names containing deeper bracket structures can still be parsed, stored, retrieved, and recomposed without calling toArray().

$params = Uri\QueryParams::parseRfc3986("a[b][c]=1");
 
$array = $params->toArray(maxNestingLevel: 2);        // ["a" => ["b" => ["c" => "1"]]]
 
$params->toArray(maxNestingLevel: 1);                 // Throws Uri\InvalidQueryParamsException

It is important to note that toArray() is the only method that interprets bracket-syntax in parameter names. All other methods (getFirst(), getAll(), has(), etc.) treat parameter names as opaque strings and do not parse brackets:

$params = Uri\QueryParams::parseRfc3986("foo%5B%5D=1&foo%5B%5D=2");
// Internally: [["foo[]", "1"], ["foo[]", "2"]]
 
$all = $params->getAll("foo[]");            // ["1", "2"]
$all = $params->getAll("foo");              // []
 
$array = $params->toArray();                // ["foo" => ["1", "2"]]  — brackets interpreted here

Class signature

The QueryParams class is final for the same reason as all the other URI classes are final: mainly, in order to make followup changes possible without breaking backward compatibility.

Additionally, QueryParams is a readonly class, meaning that instances are immutable value objects: modification methods such as append(), set(), and delete() return a new QueryParams instance rather than mutating the existing one. This design is consistent with the immutable value-object semantics of Uri\Rfc3986\Uri and Uri\WhatWg\Url, prevents different layers of an application from accidentally observing each other's modifications when sharing a reference to the same instance, and makes modification explicit at the call site. Modification methods whose name doesn't start with a “with” suffix are marked with [\NoDiscard] to warn end users not to forget to use the result. These methods (append(), set(), delete(), deleteValue()) are “withers” in fact, but using the usual prefix would have made the method names sound clunky (e.g. withAppend() or withAppendedParam()).

The QueryParams class implements the IteratorAggregate and the Countable interfaces. Implementing IteratorAggregate seems straightforward at the first sight (query parameter names could be returned as iterator keys, while query parameter values could be returned as iterator values), unfortunately, it's more tricky than that due to query components that share the same name, e.g.: param=foo&param=bar&param=baz. In this case, the same key (param) would be repeated 3 times - which would actually be very unwieldy behavior.

That's why the iterator returns each query parameter as a two-element array containing its name and value. Similarly to the get*() methods, the iterator returns the “raw” parameter names and values without percent-encoding. Let's see an example:

$params = Uri\QueryParams::parseRfc3986("param=foo&param=bar&param=baz");
 
foreach ($params as [$name, $value]) {
    echo "$name => $value\n";
}
 
/*
param => foo
param => bar
param => baz
*/

Cloning

Cloning of QueryParams is supported.

$params1 = Uri\QueryParams::parseRfc3986("foo=bar&foo=baz");
$params2 = clone $params1;
 
var_dump($params1 === $params2); // bool(false)

Serialization

QueryParams supports serialization and deserialization via the new serialization API. The only implementation gotcha is that the serialized format is slightly unexpected: instead of recomposing the query parameters into a query string, the individual query parameter name and value pairs are serialized as an array of key-value pairs, similarly to the output of the list() method. During deserialization, the query parameter list is directly created from this array without any transformation. Building options (Uri\QueryParams::$buildingOptions) are also preserved.

The main advantage of this choice is that the query parameters can be serialized and deserialized as-is, without any modifications (remember, the recomposition algorithms must percent-encode their output, and percent-decoding is needed during parsing, both of which processes modify the original data). Additionally, this behavior is more efficient than the former one, because it eliminates the overhead of parsing, including percent-encoding and decoding.

Debugging

QueryParams contains a __debugInfo() method that returns all items in the query parameter list in order to make debugging easier. Effectively, this has a similar output to the list() method.

$params = Uri\QueryParams::parseRfc3986("foo=bar&foo=baz&foo=qux");
var_dump($params);
 
/*
object(Uri\QueryParams)#1 (1) {
  ["params"]=> array(3) {
    [0]=>
    array(2) {
      [0]=>
      string(3) "foo"
      [1]=>
      string(3) "bar"
    }
    [1]=>
    array(2) {
      [0]=>
      string(3) "foo"
      [1]=>
      string(3) "baz"
    }
    [2]=>
    array(2) {
      [0]=>
      string(3) "foo"
      [1]=>
      string(3) "qux"
    }
  }
}
*/

Relation to $_GET

The $_GET superglobal stores the query parameters of the current request, percent decoded according to RFC 1866. That's why the proposed QueryParams class is its direct alternative when it comes to processing the current request. The current RFC lays the foundations for populating $_GET according to the other relevant specifications besides RFC 1866 (RFC 3986 and WHATWG URL), for example, by adding support for a new php.ini configuration option.

The position of this RFC though is that $_GET (and superglobals in general) shouldn't be changed in any way, but rather gradually phased out in the long term by offering better alternatives. In this case, QueryParams can be used directly instead of $_GET, so migrating away from the superglobal usage should be straightforward in most cases.

Given the following piece of code:

$order = isset($_GET["order"]) ? (string) $_GET["order"] : null;
$limit = isset($_GET["limit"]) ? (int) $_GET["limit"] : null;

It becomes possible to migrate to the new API roughly the following way:

$queryParams = Uri\QueryParams::parseRfc1866($_SERVER["QUERY_STRING"] ?? "");
 
$order = $queryParams->has("order") ? ($queryParams->getLast("order") ?? "") : null;
$limit = $queryParams->has("limit") ? (int) $queryParams->getLast("limit") : null;

There are a few behavioral incompatibilities between $_GET and Uri\QueryParams though that developers should be aware of when migrating:

  • Key mangling: $_GET silently replaces dots (.) and spaces in parameter names with underscores, because PHP variable names cannot contain these characters. This means that my.key=1 and my_key=1 are indistinguishable in $_GET, and data is silently corrupted. QueryParams preserves decoded parameter names without replacing dots or spaces.

  • Duplicate key loss: $_GET only retains the last value of a parameter when the same name appears multiple times (e.g. a=1&a=2 results in [“a” => “2”]). QueryParams preserves all occurrences and provides getFirst(), getLast(), and getAll() to retrieve them individually.

  • Input truncation: When the number of input variables exceeds the max_input_vars ini directive (default: 1000), $_GET discards the remaining parameters along with emitting an E_WARNING, potentially causing data loss that is difficult to detect. QueryParams also enforces a limit, but it throws an exception.

  • Input separators: $_GET respects the arg_separator.input INI directive, while all QueryParams::parse*() methods use only & as a parameter separator.

Backward Incompatible Changes

All the proposed features are backward compatible with existing code.

Proposed PHP Version(s)

Next minor version after PHP 8.6 (likely PHP 8.7).

RFC Impact

To the Ecosystem

Existing applications and libraries do not require any changes. Adoption of the new API is optional.

To Existing Extensions

Existing extensions that manipulate or access query parameters can continue to use the $_GET superglobal without any changes, however, they are encouraged to migrate to the newly added Uri\QueryParams class and its own API.

To SAPIs

None. SAPIs should continue to fill in the raw query string to the sapi_globals.request_info.query_string global variable.

Open Issues

None.

Future Scope

  • Adding support for passing objects to QueryParams: if a class implements a new interface (QueryStringable?), then it could become possible to serialize the object when appending/setting it to QueryParams.

Voting Choices

The vote starts on 2026-xx-xx, ends on 2026-xx-xx, and requires 2/3 majority to be accepted.

Vote: Add support for query parameter manipulation as outlined in the RFC? — Yes: 0 · No: 0 · Abstain: 0 (closed)

Implementation

After the RFC is implemented, this section should contain:

  1. the version(s) it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature

References

Rejected Features

  • Bracket-aware parameter manupulation methods: Dedicated methods such as getArray(), appendArray(), and setArray() were rejected because of their complexity which would have required the reimplementation of native PHP array operations. The simplified Array API provides conversion between parameter lists and PHP arrays instead.

Changelog

  • Replaced the originally proposed bracket-aware getArray(), appendArray(), and setArray() methods with fromArray(), toArray(), and withArray(). Array manipulation is performed through native PHP arrays.