# [RFC] [Discussion] Query Parameter Manipulation Support

- Thread: https://externals.io/message/130217
- Original archive: https://news-web.php.net/php.internals/130217
- Started by kocsismate90@gmail.com on 2026-03-01T22:31:10+00:00
- Messages: 16
- RFC: Query Parameter Manipulation Support (Under discussion) — https://externals.io/rfc/query_params.md
- RFC: Followup Improvements for ext/uri (Accepted) — https://externals.io/rfc/uri_followup.md
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #130217 — kocsismate90@gmail.com — 2026-03-01T22:31:10+00:00

URL: https://externals.io/message/130217#130217

Hey Everyone,

As I mentioned in a previous email of mine (
https://externals.io/message/129486#130077),
I recently separated the query parameter handling sub-proposal from
https://wiki.php.net/rfc/uri_followup into its own RFC because it was way
too complex.
Therefore I'm officially opening its discussion.

After the separation, I reworked the proposal quite a lot: the single
biggest change
is that now, only a single class would be added: Uri\QueryParams instead of
both an RFC 3986
and a WHATWG URL compatible implementation.

The focus of the RFC is now to move away from the usage of the $_GET
superglobal,
which goal comes with two additional expectations:
- the new implementation should have comparable performance to $_GET
- the new implementation should support most capabilities of $_GET (e.g.
arrays)

The first one is probably straightforward to achieve, the latter one has
fundamental
problems: PHP's feature set (mostly: array support) is not compatible with
the WHATWG URL,
so some behavior likely wouldn't comply with this specification. That's why
the RFC
still has some TBD parts (e.g. Array API), or some contradicting info
related to some getters' and setters' signature/behavior.

Other than the API itself, the proposal doesn't have many questions, except
one thing: whether the new class should be readonly or not? I tend to make
it readonly, but I'm still not sure (since
this class can be used as a Builder).

Regards,
Máté

---

## #130218 — tim@bastelstu.be — 2026-03-01T22:43:46+00:00

URL: https://externals.io/message/130217#130218 · In reply to: #130217

Hi


> Therefore I'm officially opening its discussion.

You forgot to link the RFC. I'm doing that here:

https://wiki.php.net/rfc/query_params

Best regards
Tim Düsterhus

---

## #130219 — tim@bastelstu.be — 2026-03-01T22:57:57+00:00

URL: https://externals.io/message/130217#130219 · In reply to: #130217

Hi


> Other than the API itself, the proposal doesn't have many questions, except
> one thing: whether the new class should be readonly or not? I tend to make
> it readonly, but I'm still not sure (since
> this class can be used as a Builder).

I don't think it is as necessary to make it readonly as it is with the 
URI classes, since the query parameters class is likely to be used for 
“local modification” and is less likely to be passed around as a value 
object. If efficient copy-on-write is possible internally, then making 
it readonly is the safer choice, of course.

With regard to the RFC text, I've given it a first quick skim and have 
the following notes:

1. Within the "Relation to the query component" section.

The `$uri = new Uri\Rfc3986\Uri("https://example.com?foo=a b");` example 
is invalid. An URI containing a space is not valid and the `Uri` class 
will throw an exception. Can you double-check that part of the RFC? 
Without looking it up, I suspect that the space should be `%20` for RFC3986?

2. Within the "Modification" section:

“Neither append(), nor set() do any percent-encoding or decoding of 
their arguments.”

This should explain that during stringification, the `%` will be encoded 
as %25. Thus the example will result in:

     foo%255B%255D=ab%2563&bar%255B%255D=de%2566

3. Within the "Modification" section:

“Finally, `sort()` sorts the query parameter list alphabetically:”

It is implied by the example, but should be spelled out explicitly: 
Parameters with the same name should keep their original order and 
should not be sorted by value.

4. Within the "Supported types" section.

“The above conversion rules work for both UriQueryParams and 
UrlQueryParams.”

and the examples also use Uri\Rfc3986\UriQueryParams / 
Uri\WhatWg\UrlQueryParams.

This seems like an oversight from the original implementation, since 
there are no longer separate classes. Can you double-check the entire 
section?

5. Within the "Array API" section.

This also uses Uri\Rfc3986\UriQueryParams in the examples.

6. Within the "RFC Impact" section.

The ecosystem impact can probably be “None”.

---------

I'll give this an in-depth read later, when the obvious mistakes are fixed.

Best regards
Tim Düsterhus

---

## #130220 — Jordi Kroon — 2026-03-01T23:14:11+00:00

URL: https://externals.io/message/130217#130220 · In reply to: #130217

Hey Máté,

Nice work, I really appreciate the effort! While adopting Uri I stumbled upon this missing feature as well.


In the examples you mention the usage of `parse` and `toString`. I assume there is no “default” and this is a placeholder for one of the given implementations. But it’s perhaps better to pick one of for the examples as it suggests it would also exist.


When it comes to the implementation I believe it would be nice to also have a `getKeys()` method. That would only return the keys.

You do mention the focus is to move away from $_GET. I like the idea, but implementing QueryString would still require something like the following:
Uri\QueryParams::fromArray($_GET);
If we really want to make `$_GET` obsolete, wouldn’t it be nice to have a `fromRequest()` which would directly parse `$_GET` or use `$_SERVER["QUERY_STRING”]`.

As for whether or not this should be a readonly class. I think it should be. The class itself is following various specs. And since all other classes in the same namespace are also readonly, it should follow the same principle (also for consistency) I believe.

Regards,
Jordi

> Hey Everyone,
>
> As I mentioned in a previous email of mine (https://externals.io/message/129486#130077),
> I recently separated the query parameter handling sub-proposal from https://wiki.php.net/rfc/uri_followup into its own RFC because it was way too complex.
> Therefore I'm officially opening its discussion.
>
> After the separation, I reworked the proposal quite a lot: the single biggest change
> is that now, only a single class would be added: Uri\QueryParams instead of both an RFC 3986
> and a WHATWG URL compatible implementation.
>
> The focus of the RFC is now to move away from the usage of the $_GET superglobal,
> which goal comes with two additional expectations:
> - the new implementation should have comparable performance to $_GET
> - the new implementation should support most capabilities of $_GET (e.g. arrays)
>
> The first one is probably straightforward to achieve, the latter one has fundamental
> problems: PHP's feature set (mostly: array support) is not compatible with the WHATWG URL,
> so some behavior likely wouldn't comply with this specification. That's why the RFC
> still has some TBD parts (e.g. Array API), or some contradicting info related to some getters' and setters' signature/behavior.
>
> Other than the API itself, the proposal doesn't have many questions, except one thing: whether the new class should be readonly or not? I tend to make it readonly, but I'm still not sure (since
> this class can be used as a Builder).
>
> Regards,
> Máté

---

## #130285 — kocsismate90@gmail.com — 2026-03-09T20:47:41+00:00

URL: https://externals.io/message/130217#130285 · In reply to: #130219

Hey Tim,

Thank you for the help, I forgot to link the RFC indeed.

Let me answer your questions:

>
> I don't think it is as necessary to make it readonly as it is with the
> URI classes, since the query parameters class is likely to be used for
> “local modification” and is less likely to be passed around as a value
> object. If efficient copy-on-write is possible internally, then making
> it readonly is the safer choice, of course.
>

Yes, as far as I know from Ilija, it's possible to optimize modification
based on
refcount: if a readonly property which is to be modified has a refcount of
1 and
there's no weakref, it can be safely updated in-place.


> With regard to the RFC text, I've given it a first quick skim and have
> the following notes:
>
> 1. Within the "Relation to the query component" section.
>
> The `$uri = new Uri\Rfc3986\Uri("https://example.com?foo=a b");` example
> is invalid. An URI containing a space is not valid and the `Uri` class
> will throw an exception. Can you double-check that part of the RFC?
> Without looking it up, I suspect that the space should be `%20` for
> RFC3986?
>

Nice catch, I'll review and fix the examples using "a b". It is indeed
incorrect, and %20
should be used instead. as you said.


> 4. Within the "Supported types" section.
>
> “The above conversion rules work for both UriQueryParams and
> UrlQueryParams.”
>
> and the examples also use Uri\Rfc3986\UriQueryParams /
> Uri\WhatWg\UrlQueryParams.
>
> This seems like an oversight from the original implementation, since
> there are no longer separate classes. Can you double-check the entire
> section?
>

I intentionally haven't updated this section yet: because there's important
info about
the difference between how WHATWG specification and uriparser handles null,
so I didn't want to "lose" this (I know it remains in the changelog).


>
> 5. Within the "Array API" section.
>
> This also uses Uri\Rfc3986\UriQueryParams in the examples


Same situation as above, but basically the whole section is a big question
mark:
I couldn't come up with a good enough idea how arrays could be handled
efficiently and ergonomically, and in the same time, how to (mostly) conform
to WHATWG URL.

Some of my problems:
- How should $params->get("foo[bar]") behave? Should we try to parse the
supplied key as an array and return the
nested item if it exists? I suppose, we would internally store the query
params as an array (just like how $_GET does),
so then we would definitely need parsing in order to be able to return item
"bar" stored within array "foo".
- However, what should a $params->append("foo[bar]") call do when the query
param "foo" is already added with a string value?
Should it modify the existing item to be an array (but then how?) or we
should just add "foo[bar]" to the query params as-is?
- How to recompose list parameters? Ideally, we should not append [] to
these parameters (e.g.
"list=1&list=2" vs "list[]=1&list[]=2"), and actually, it is not needed for
arrays in the root level, but as far as I understood,
it's a must-have for nested lists (e.g. "map[list]=1&map[list]=2" vs.
map[list[]]=1&map[list[]]=2), otherwise the representation
would be ambiguous for lists with 1 item: "map[list]=1" would mean that the
map has a list item with a value of "1". So
we either always have to use the [] suffix for lists, or we should make a
distinction between root-level and nested lists.

I know, most of my questions revolve around parsing the $name parameter,
but I would really want to minimize the number
of times when the $name param has to be parsed run-time so that the
performance is comparable to native array key
lookups. I don't have any idea how much the parsing would cost though...

So at this point, coming up with a sensible plan is needed, and I can fix
most inconsistencies in the RFC afterwards.

Regards,
Máté

---

## #130286 — kocsismate90@gmail.com — 2026-03-09T21:05:04+00:00

URL: https://externals.io/message/130217#130286 · In reply to: #130220

Hi Jordi,

In the examples you mention the usage of `parse` and `toString`. I assume
> there is no “default” and this is a placeholder for one of the given
> implementations. But it’s perhaps better to pick one of for the examples as
> it suggests it would also exist.
>

Ah, thanks for the info! Most of these were omission in fact, as I forgot
to update the original method names after I unified the two QueryParams
classes.


> When it comes to the implementation I believe it would be nice to also
> have a `getKeys()` method. That would only return the keys.
>
> You do mention the focus is to move away from $_GET. I like the idea, but
> implementing QueryString would still require something like the following:
>
> Uri\QueryParams::fromArray($_GET);
>
> If we really want to make `$_GET` obsolete, wouldn’t it be nice to have a
> `fromRequest()` which would directly parse `$_GET` or use
> `$_SERVER["QUERY_STRING”]`.
>

As far as I can see, Uri\QueryParams::fromArray($_GET); wouldn't be needed.
Do you have any use-case in mind where the suggested
"Uri\QueryParams::parseRfc3986($_SERVER["QUERY_STRING"]);" call wouldn't
work instead? I'm not against adding a fromRequest()
method though if others find it useful (I'm a bit neutral about it). And
I'd probably use a fromCurrentRequest() or a similar name to
highlight the fact that it uses the current request as source.


> As for whether or not this should be a readonly class. I think it should
> be. The class itself is following various specs. And since all other
> classes in the same namespace are also readonly, it should follow the same
> principle (also for consistency) I believe.
>

I think the most problematic case is to return a mutable class from a
readonly class (e.g. Uri\Rfc3986\Uri::getQueryParams()). So we should either
omit this method, or make Uri\QueryParams readonly indeed.

Regards,
Máté

---

## #130376 — tim@bastelstu.be — 2026-03-16T21:04:57+00:00

URL: https://externals.io/message/130217#130376 · In reply to: #130285

Hi


> Same situation as above, but basically the whole section is a big question
> mark:
> I couldn't come up with a good enough idea how arrays could be handled
> efficiently and ergonomically, and in the same time, how to (mostly) conform
> to WHATWG URL.

The internal storage should not know about arrays. It should represent 
things as a list of key-value pairs, with arrays only being materialized 
for the methods that are intended to deal with arrays.

Given that the class is generic, matching the WHATWG API 1:1 is not 
necessary in my opinion.

> Some of my problems:
> - How should $params->get("foo[bar]") behave? Should we try to parse the
> supplied key as an array and return the
> nested item if it exists? I suppose, we would internally store the query
> params as an array (just like how $_GET does),
> so then we would definitely need parsing in order to be able to return item
> "bar" stored within array "foo".

`->get()` does not currently exist within the RFC. The WHATWG `get()` is 
equivalent to `->getFirst()`.

> - However, what should a $params->append("foo[bar]") call do when the query
> param "foo" is already added with a string value?
> Should it modify the existing item to be an array (but then how?) or we
> should just add "foo[bar]" to the query params as-is?

It should add `foo[bar]` as-is. `foo` and `foo[bar]` are different keys. 
The square-bracket syntax is something that is specific to PHP.

> - How to recompose list parameters? Ideally, we should not append [] to
> these parameters (e.g.

The recomposition should be "dumb" and just use the keys as-is. The 
complex logic should happen in the getter or setter. Only the 'array' 
getters and setters deal with the PHP-specific square-bracket syntax.

To give an example:

     "k=1&k=2"
     [['k', '1'], ['k', '2']]
     getAll('k')   // ['1', '2']
     getArray('k') // UriException: 'k' is not an array.

     "k[]=1&k[]=2"
     [['k[]', '1'], ['k[]', '2']]
     getAll('k')   // []
     getAll('k[]') // ['1', '2']
     getArray('k') // ['1', '2']

     "k[a]=1&k[b]=2"
     [['k[a]', '1'], ['k[b]', '2']]
     getAll('k')    // []
     getAll('k[a]') // ['1']
     getArray('k')  // ['a' => '1', 'b' => '2']

     "k=1&k[]=2"
     [['k', '1'], ['k[]', '2']]
     getAll('k')   // ['1']
     getAll('k[]') // ['2']
     getArray('k') // UriException: 'k' is not an array.

     "k[]=2&k=3";
     [['k[]', '2'], ['k', '3']];
     getAll('k')   // ['3']
     getAll('k[]') // ['2']
     getArray('k') // UriException: 'k' is not an array.

The string should internally be represented something like the arrays 
and the getters should work as indicated in the comment.

And then for the setters:

     // []
     append('k', '1')   // [['k', '1']]
     append('k', '2')   // [['k', '1'], ['k', '2']]
     append('k[]', '3') // [['k', '1'], ['k', '2'], ['k[]', '3']]
     set('k', '4')      // [['k[]', '3'], ['k', '4']]

The `setArray()` method would then create a number of entries with 
square brackets added as appropriate (and delete all entries that would 
be returned by `getArray()`). The `appendArray()` method should probably 
be dropped to avoid issues with "mixing non-array with array keys". So:

     // []
     setArray('k', ['1'])             // [['k[]', '1']]
     setArray('k', ['1', '2'])        // [['k[]', '1'], ['k[]', '2']]
     setArray('k', ['a' => '1', '2']) // [['k[a]', '1'], ['k[0]', '2']]
     append('k', 'x')                 // [
                                      //  ['k[a]', '1'],
                                      //  ['k[0]', '2'],
                                      //  ['k', 'x'],
                                      // ]
     setArray('k', ['1'])             // [['k[]', '1']]

Does that make sense?

Best regards
Tim Düsterhus

---

## #131304 — kocsismate90@gmail.com — 2026-06-15T20:36:47+00:00

URL: https://externals.io/message/130217#131304 · In reply to: #130376

Hi Tim,

First of all, thanks for your detailed explanation!

Does that make sense?
>

Sure, this is a very sensible approach, similar to the algorithms I have
also considered myself before. The reason why I
started to search for alternatives is because lookups are inefficient this
way: all get*() methods have an O(n) time complexity
by default, while currently, looking up keys in $_GET is O(1). This
regression is what I tried to avoid with storing query params
along with an array key.

However, I realized in the meanwhile that it may be possible to eliminate
much of the performance overhead by using two hash tables:
one for exact search (WHATWG URL-alike API), and one for searching the
prefixes before the [] pair (for the array API). I haven't
tried this idea out yet in practice, so I'll need to do some POC first
before continuing with the RFC.

Regards,
Máté

---

## #131825 — kocsismate90@gmail.com — 2026-07-08T21:28:04+00:00

URL: https://externals.io/message/130217#131825 · In reply to: #131304

Hello Tim et All,

I managed to implement a considerable part of the proposal efficiently, as
I planned. However, I came to a different conclusion
regarding the "Array API" (https://wiki.php.net/rfc/query_params#array_api)
than what I originally wrote:

First and foremost, I wasn't happy that the Array API would try to mimic
native array handling. For example, QueryParams::getArray("foo[bar][baz]")
would try to parse the supplied array dimensions and then find index "baz"
of index "bar" of array "foo". QueryParams::appendArray() and
QueryParams::setArray() would need a similar behavior. I wasn't keen on
reimplementing array operations within query params. In order to solve
this Gordian knot, I want to simplify the Array API to only two (or maybe
three) methods:
- QueryParams::fromArray(): Creates a new QueryParams object from an array
- QueryParams::withArray(): Does the same as above, but from an existing
instance (I'm not 100% sure about this method yet)
- QueryParams::toArray(): Creates an array from the QueryParams instance

If someone wants to do any kind of array modifications, then they can do it
by retrieving the query params array first via toArray(), change what's
needed
by using PHP's native array manipulation functionalities, and finally,
convert the.array back to a QueryParams instance. I think this behavior is
simple, clear,
and also extensible later, if we learn about a better approach...

Besides, I also realized that supporting some kind of configuration will
also be necessary mostly for security reasons. These are the options
I'm considering to add:

- parsingMaxQueryStringLength: the maximum length of the input query string
that can be parsed by the parse*() methods
- parsingMaxParamCount: The maximum number of parameters that the input
query string can contain when calling any parse*() methods (similar
to the "max_input_vars" php.ini option).
- parsingMaxNestingLevel: The maximum nesting level of arrays that can be
parsed when calling any parse*() methods (similar
to the "max_input_nesting_level" php.ini option)

I figured that it might be useful to control how some specific type of
query parameter values are represented when adding a new query
parameter via  calling e.g. QueryParams::append("param", $value).

- boolean values: by default, true would be represented as "1" and false
would be represented as "0"
- null values: there are three ways to handle them: 1) omitting these; 2)
adding them without a "=", containing only the param name (e.g. "?foo");
3) and converting the null value to an empty string (e.g. "?foo=")

I'm not really sure how useful these options are, so I'm curious to get
feedback about them. It's also an option to only accept string or null
values by the manipulator methods, but I think it would provide a better UX
to support all valid types (e.g. an int is a valid one, but an object
isn't).

It's also a question how and where these configs should be passed.
Currently, I group them together in a dedicated QueryParamOptions object
which has the following signature:

final readonly class QueryParamOptions
{
        public function __construct(
            public int $parsingMaxQueryStringLength = 10000,
            public int $parsingMaxParamCount = 1000,
            public int $parsingMaxNestingLevel = 64,
            public string $trueValue = "0",
            public string $falseValue = "0",
            public bool $useNullAsEmptyString = false
        ) {}}

However, the properties prefixed by "parsing" are only applicable during
parsing, so maybe they should be passed separately to the parse*() methods?
What are your thoughts?

Regards,
Máté

---

## #131908 — ignace nyamagana butera — 2026-07-14T06:15:42+00:00

URL: https://externals.io/message/130217#131908 · In reply to: #131825

> Hello Tim et All,
>
> I managed to implement a considerable part of the proposal efficiently, as
> I planned. However, I came to a different conclusion
> regarding the "Array API" (https://wiki.php.net/rfc/query_params#array_api)
> than what I originally wrote:
>
> First and foremost, I wasn't happy that the Array API would try to mimic
> native array handling. For example, QueryParams::getArray("foo[bar][baz]")
> would try to parse the supplied array dimensions and then find index "baz"
> of index "bar" of array "foo". QueryParams::appendArray() and
> QueryParams::setArray() would need a similar behavior. I wasn't keen on
> reimplementing array operations within query params. In order to solve
> this Gordian knot, I want to simplify the Array API to only two (or maybe
> three) methods:
> - QueryParams::fromArray(): Creates a new QueryParams object from an array
> - QueryParams::withArray(): Does the same as above, but from an existing
> instance (I'm not 100% sure about this method yet)
> - QueryParams::toArray(): Creates an array from the QueryParams instance
>
> If someone wants to do any kind of array modifications, then they can do
> it by retrieving the query params array first via toArray(), change what's
> needed
> by using PHP's native array manipulation functionalities, and finally,
> convert the.array back to a QueryParams instance. I think this behavior is
> simple, clear,
> and also extensible later, if we learn about a better approach...
>
> Besides, I also realized that supporting some kind of configuration will
> also be necessary mostly for security reasons. These are the options
> I'm considering to add:
>
> - parsingMaxQueryStringLength: the maximum length of the input query
> string that can be parsed by the parse*() methods
> - parsingMaxParamCount: The maximum number of parameters that the input
> query string can contain when calling any parse*() methods (similar
> to the "max_input_vars" php.ini option).
> - parsingMaxNestingLevel: The maximum nesting level of arrays that can be
> parsed when calling any parse*() methods (similar
> to the "max_input_nesting_level" php.ini option)
>
> I figured that it might be useful to control how some specific type of
> query parameter values are represented when adding a new query
> parameter via  calling e.g. QueryParams::append("param", $value).
>
> - boolean values: by default, true would be represented as "1" and false
> would be represented as "0"
> - null values: there are three ways to handle them: 1) omitting these; 2)
> adding them without a "=", containing only the param name (e.g. "?foo");
> 3) and converting the null value to an empty string (e.g. "?foo=")
>
> I'm not really sure how useful these options are, so I'm curious to get
> feedback about them. It's also an option to only accept string or null
> values by the manipulator methods, but I think it would provide a better
> UX to support all valid types (e.g. an int is a valid one, but an object
> isn't).
>
> It's also a question how and where these configs should be passed.
> Currently, I group them together in a dedicated QueryParamOptions object
> which has the following signature:
>
> final readonly class QueryParamOptions
> {
>         public function __construct(
>             public int $parsingMaxQueryStringLength = 10000,
>             public int $parsingMaxParamCount = 1000,
>             public int $parsingMaxNestingLevel = 64,
>             public string $trueValue = "0",
>             public string $falseValue = "0",
>             public bool $useNullAsEmptyString = false
>         ) {}}
>
> However, the properties prefixed by "parsing" are only applicable during
> parsing, so maybe they should be passed separately to the parse*() methods?
> What are your thoughts?
>
> Regards,
> Máté
>
> Hi Máté,
thanks for the feedback and for the work already done on this RFC.

>
>  - QueryParams::fromArray(): Creates a new QueryParams object from an
> array
> - QueryParams::withArray(): Does the same as above, but from an existing
> instance (I'm not 100% sure about this method yet)
> - QueryParams::toArray(): Creates an array from the QueryParams instance


IMHO the suffix "Array" is an implementation detail I would prefer
something like "fromQueryParams", "toQueryParams" and
"(with|merge)QueryParams" . This conveys more the nature of the operations
than the type of operation.

I think your reasoning to simplify this part of the RFC makes sense instead
of adding a complex API users may want to rely on their own "Collection"
like object structure to deal with Query parameters
modification as long as they adhere to the principle of only
allowing scalar type and null as possible values.

final readonly class QueryParamOptions
> {
>         public function __construct(
>             public int $parsingMaxQueryStringLength = 10000,
>             public int $parsingMaxParamCount = 1000,
>             public int $parsingMaxNestingLevel = 64,
>             public string $trueValue = "0",
>             public string $falseValue = "0",
>             public bool $useNullAsEmptyString = false
>         ) {}}


This seems to me 2 pairs of options: One used for parsing, the other for
building. I would split this class into two QueryParamParsingOptions and
QueryParamsBuildingOptions, names can be improved.

final readonly class QueryParamParsingOptions
{
        public function __construct(
            public int $parsingMaxQueryStringLength = 10000,
            public int $parsingMaxParamCount = 1000,
            public int $parsingMaxNestingLevel = 64,
        ) {}
}

And

final readonly class QueryParamBuildingOptions
{
        public function __construct(
            public string $trueValue = "1",
            public string $falseValue = "0",
            public bool $useNullAsEmptyString = false
        ) {}
}

The QueryParamParsingOptions should be an argument for all your `parse*`
methods with sensible default value the same for the QueryParamBuildingOptions
but for the`to*` methods. The behaviour toward
using `null` seems unclear to me when checking the class on itself. partly
because it is a boolean, partly because you state that there's 3 ways to
handle null values and a boolean only allows for 2 ways. Perhaps
using an Enum would be a better fit in this case ?

Best regards,
Ignace

---

## #131915 — ignace nyamagana butera — 2026-07-14T11:14:44+00:00

URL: https://externals.io/message/130217#131915 · In reply to: #131908

On Tue, Jul 14, 2026 at 8:15 AM ignace nyamagana butera <nyamsprod@gmail.com>
wrote:

>
>
> On Wed, Jul 8, 2026 at 11:31 PM Máté Kocsis <kocsismate90@gmail.com>
> wrote:
>
> > […]
> thanks for the feedback and for the work already done on this RFC.
>
> > […]
>
>
> IMHO the suffix "Array" is an implementation detail I would prefer
> something like "fromQueryParams", "toQueryParams" and
> "(with|merge)QueryParams" . This conveys more the nature of the operations
> than the type of operation.
>
> I think your reasoning to simplify this part of the RFC makes sense
> instead of adding a complex API users may want to rely on their own
> "Collection" like object structure to deal with Query parameters
> modification as long as they adhere to the principle of only
> allowing scalar type and null as possible values.
>
> final readonly class QueryParamOptions
> > […]
>
>
> This seems to me 2 pairs of options: One used for parsing, the other for
> building. I would split this class into two QueryParamParsingOptions and
> QueryParamsBuildingOptions, names can be improved.
>
> final readonly class QueryParamParsingOptions
> {
>         public function __construct(
>             public int $parsingMaxQueryStringLength = 10000,
>             public int $parsingMaxParamCount = 1000,
>             public int $parsingMaxNestingLevel = 64,
>         ) {}
> }
>
> And
>
> final readonly class QueryParamBuildingOptions
> {
>         public function __construct(
>             public string $trueValue = "1",
>             public string $falseValue = "0",
>             public bool $useNullAsEmptyString = false
>         ) {}
> }
>
> The QueryParamParsingOptions should be an argument for all your `parse*`
> methods with sensible default value the same for the QueryParamBuildingOptions
> but for the`to*` methods. The behaviour toward
> using `null` seems unclear to me when checking the class on itself. partly
> because it is a boolean, partly because you state that there's 3 ways to
> handle null values and a boolean only allows for 2 ways. Perhaps
> using an Enum would be a better fit in this case ?
>
> Best regards,
> Ignace
>

Hi Máté,

As a follow up I would propose the following Enum but again names can be
change/improve

namespace Uri;

 enum QueryNullPolicy {
     case SkipTuple;
     case KeyOnly;
     case EmptyString;
}

and the class becomes

final readonly class QueryParamBuildingOptions
{
        public function __construct(
            public string $trueValue = "1",
            public string $falseValue = "0",
            public QueryNullPolicy $nullPolicy = QueryNullPolicy::SkipTuple
        ) {}
}

which is more readable and understandable IMHO

Best regards,
Ignace

---

## #131946 — kocsismate90@gmail.com — 2026-07-15T20:34:25+00:00

URL: https://externals.io/message/130217#131946 · In reply to: #131915

Hi Ignace,

Thanks for your feedback!

I'm fine with separating the options to their own object. However, what I
would like to make sure about first is whether
the limits should really be applicable for parsing methods only, or rather
other methods should also respect them?

- $parsingMaxQueryStringLength: This option is only useful during parsing,
so it's out of scope for my question.
- $parsingMaxParamCount: the append() and set() modification methods could
also respect a $maxParamCount option,
triggering an exception when the query parameter list exceeds the max
parameter count.
- $parsingMaxNestingLevel: similarly, append() and set() could also respect
it (e.g. by counting the number
of opening brackets in the query param name (?)). Enforcing this option
during parsing and modification would result in
some performance penalty, since the param name doesn't need introspection
during these operations otherwise.
That's why an alternative is to make a $maxNestingLevel option exclusive to
the toArray() method, because that's the
only time nesting level actually comes into play (and the brackets need
parsing at that point anyway). The possible problem
with this delayed validation is that possibly harmful query strings can be
passed along more easily.

The reason why I originally thought about supporting these options during
only parsing is because I'd think that
that's the most uncontrollable part of query parameter handling. E.g. one
can enforce a maximum query param count
easily after parsing if this is needed (e.g. checking `count()` before
calling append() or set()).

... the same for the QueryParamBuildingOptions but for the`to*` methods.


Actually, the building options should be passed to append() and set(). It's
because their $value parameter is type juggled to
string or null immediately, thus their original type gets lost afterwards.

On one hand, this behavior is useful for the hasValue() or deleteValue()
methods which try to find their $value param in the
query param list (comparing the type juggled $value with the stored values
-> a boolean "true" $value will be equal to a string "1"
in the list without the need to type juggle all stored query params).

On the other hand though, this behavior might lead to interoperability
issues, e.g. when two libraries use different building options
to write to the same QueryParams.... Although, I think this is also a
possible problem even if building options are not supported,
so probably it's not a blocking issue. But most probably, passing them
during construction would slightly alleviate this problem,
because all writes would use the exact same config.

Regards,
Máté

---

## #131958 — ignace nyamagana butera — 2026-07-16T08:13:07+00:00

URL: https://externals.io/message/130217#131958 · In reply to: #131946

> Hi Ignace,
>
> Thanks for your feedback!
>
> I'm fine with separating the options to their own object. However, what I
> would like to make sure about first is whether
> the limits should really be applicable for parsing methods only, or
> rather other methods should also respect them?
>
> - $parsingMaxQueryStringLength: This option is only useful during
> parsing, so it's out of scope for my question.
> - $parsingMaxParamCount: the append() and set() modification methods
> could also respect a $maxParamCount option,
> triggering an exception when the query parameter list exceeds the max
> parameter count.
> - $parsingMaxNestingLevel: similarly, append() and set() could also
> respect it (e.g. by counting the number
> of opening brackets in the query param name (?)). Enforcing this option
> during parsing and modification would result in
> some performance penalty, since the param name doesn't need introspection
> during these operations otherwise.
> That's why an alternative is to make a $maxNestingLevel option exclusive
> to the toArray() method, because that's the
> only time nesting level actually comes into play (and the brackets need
> parsing at that point anyway). The possible problem
> with this delayed validation is that possibly harmful query strings can
> be passed along more easily.
>
> The reason why I originally thought about supporting these options during
> only parsing is because I'd think that
> that's the most uncontrollable part of query parameter handling. E.g. one
> can enforce a maximum query param count
> easily after parsing if this is needed (e.g. checking `count()` before
> calling append() or set()).
>
> ... the same for the QueryParamBuildingOptions but for the`to*` methods.
>
>
> Actually, the building options should be passed to append() and set().
> It's because their $value parameter is type juggled to
> string or null immediately, thus their original type gets lost afterwards.
>
> On one hand, this behavior is useful for the hasValue() or deleteValue()
> methods which try to find their $value param in the
> query param list (comparing the type juggled $value with the stored
> values -> a boolean "true" $value will be equal to a string "1"
> in the list without the need to type juggle all stored query params).
>
> On the other hand though, this behavior might lead to interoperability
> issues, e.g. when two libraries use different building options
> to write to the same QueryParams.... Although, I think this is also a
> possible problem even if building options are not supported,
> so probably it's not a blocking issue. But most probably, passing them
> during construction would slightly alleviate this problem,
> because all writes would use the exact same config.
>
> Regards,
> Máté
>

Hi Màté,

I believe the question is more about domain boundaries.

AFAIK:

- HTTP servers already have a cap on how long an URL and thus a query
string can be
- the query string nesting or parsing depends only on how it is being
consumed by the client and should not be restricted by the URI producer.

Caveats:

- the supported URI length depends on the server, CLI scripts and user
scripts are able to bypass those HTTP server restrictions.

Here's my thoughts:

- Parsing and building are at the boundary of the system and control the
I/O flow
- Parsing makes the system behave like a consumer, Building makes the
system behave like a producer.
- I consider that dynamic methods are part of the domain because they only
deal with PHP data not with external data (we do not allow concatenating 2
query string for instance)

So I would not add the parsing options to the class modifier because on
itself PHP should not add more constraints than needed. business rules might
but IMHO this is out of scope for a low level API.

Does PHP work with more nested levels or query parameters, the answer is
yes. Should we recommend doing so, hell no but this is a business
constraint unless it is a clear and known security issue when building the
query string.

On the other hand, I do agree that being able to configure how boolean and
null values are handled when using any modifier methods is a needed feature.

TL;DR:

Adding the `QueryParamBuildingOptions` options as the last argument with
sensible default to any modifier methods seems reasonable to me

but adding

public int $maxParamCount = 1000,
public int $maxNestingLevel = 64,

to `QueryParamBuildingOptions` without a clear and realistic example where
this is solving a security issue seems like too much of a restriction for
the API.

Best regards,
Ignace

---

## #132810 — kocsismate90@gmail.com — 2026-10-06T13:37:52+00:00

URL: https://externals.io/message/130217#132810 · In reply to: #131958

Hi Ignace,

Finally, I had the capacity to work on this proposal again, after the PHP
8.6 release is now close to the finish line.

I made a lot of changes to the QueryParams RFC, covering a lot of edge
cases, and explicitly documenting many so far undefined behavior. Here is a
short "changelog":

- Immutability: QueryParams is now documented to be readonly (previously it
was TBD)
- Null handling: As proposed by Ignace, QueryParamNullPolicy replaces the
boolean option with three choices: SkipTuple, KeyOnly (the default), and
EmptyString
- Limits and errors: Parsing limits apply only during parsing, while the
nesting limit belongs exclusively to toArray(). Exception behavior is now
specified
- Type conversions: Type conversion rules are documented much more
thoroughly than before
- Array API: Previously open questions are decided and documented,
including nested lists, duplicate keys, conflicting structures, and
malformed bracket notation
- URI/URL integration: The Uri/Url classes will have getQueryParams() and
withQueryParams() methods to make usage and interoperability easier
- Parsing details: The RFC now specifies the handling of empty fields,
missing values, malformed percent-encoding, and invalid UTF-8 etc.

Please give the RFC a thorough read, because I'd like to bring this
proposal to a vote as soon as possible (but not sooner ^^), of course still
giving interested folks a chance
to provide feedback. I'm specifically looking forward to naming
improvements, if there's anything to improve.

Regards,
Máté

---

## #132851 — tim@bastelstu.be — 2026-10-09T12:05:00+00:00

URL: https://externals.io/message/130217#132851 · In reply to: #130217

Hi


> As I mentioned in a previous email of mine (
> https://externals.io/message/129486#130077),
> I recently separated the query parameter handling sub-proposal from
> https://wiki.php.net/rfc/uri_followup into its own RFC because it was 
> way
> too complex.
> Therefore I'm officially opening its discussion.

I've given the RFC a fresh read from scratch after your email 
https://news-web.php.net/php.internals/132810. I'm replying to the top 
of the thread to reduce the nesting, and because I'm not replying to 
anyone or anything in particular. Some collected thoughts roughly in 
order they appear in the RFC:

1. I don't think `QueryParamParsingOptions::$maxQueryStringLength` is 
necessary or useful to have. Checking the input length is easy to do 
manually (it's just a `strlen()` call) and I don't see a practical way 
to abuse the API with large input lengths alone: The API will not “blow 
up” inputs into considerably larger data structures, the input strings 
are mostly just “copied” over, not too different from e.g. 
`str_replace()`.

2. The `$maxParamCount` on the other hand makes more sense to me, but 
even for that the number of “entries” will be at most the input length 
(for `&&&`).

3. If `$maxQueryStringLength` goes away, the `QueryParamParsingOptions` 
would be a single property object. Maybe the param count limit should 
become a regular parameter then, since I can't think of any other useful 
parse-time options.

4. For the `QueryParamBuildingOptions` I don't think they should be 
passed as parameter to the parsing functions (and thus not be stored as 
an object property). Instead they should be passed to the `->to*()` 
functions just when they are needed. That keeps the QueryParams class’ 
state focused on the actual parameters.

Edit after getting further down the RFC: This choice is explained 
further below, because it immediately affects the getters. This makes 
sense, but I still believe this is a less than ideal API design. But I 
don't have better proposals other than “force the user to make a 
decision and only allow passing in `string`”. For `int` the implicit 
conversion to string would just work and making an explicit choice for 
`bool` is easy enough.

5. Is having a separate `parseRfc3986()` useful when RFC 3986 doesn't 
specify the handling of query parameters and just delegates to RFC 1866 
instead? A big benefit of the URI extension for me is that it clearly 
references the relevant standards in its naming and strictly implements 
them, making it easy to discover and allowing the use of primary 
references to understand what they are supposed to do. The 
`parseRfc3986()` method would muddy this straight-forward standard’s 
compliant implementation, particularly since it is defined to accept 
malformed inputs such as `#foo=bar`, which I would find unexpected from 
a method referencing RFC 3986 in its name.

6. The Uri::getQueryParams() method on the other hand makes sense to me. 
It can use RFC 1866 as an implementation detail after performing the 
correct percent decoding.

7. I cannot describe an actual issue, but I find accepting invalid 
percent-encoding sequences to be dangerous. It would definitely be 
unexpected that `%6f=x' would be `o=x`, but `%6g=y` would be `%6g=y` 
(which would then become `%256g=y`).

8. The naming of `hasValue()` sounds like it would check if there is a 
`=` sign for the given key somewhere, not that it would check that there 
is a specific key-value pair. I'm also not sure if it is necessary. 
Duplicated keys are comparatively rare and for the use cases where this 
is a thing, `in_array()` on the result of `getAll()` would just work (or 
`array_any()` for more complex checks).

9. I'm not sure I like the name `->list()`. Technically it's also 
redundant with `iterator_to_array($queryParams)`, but having a dedicated 
method probably makes sense nevertheless.

10. Would it make sense to rename `parseWhatWg()` to 
`parseUrlSearchParams()` or similar to make it clear that this 
constructor implements the URLSearchParams specification. I'm asking due 
to the section that mentioned how `$whatWgUrl->getQuery()` is not the 
correct value to pass there (due to the questionmark handling). Renaming 
this method would also make sense when removing the RFC 3986 parser (as 
suggested in (5)).

11. Bullet point 8 also applies to `deleteValue()`. Is there an actual 
use case for this method?

12. Is having the `withArray()` method useful? It does the same as 
`QueryParams::fromArray()`, no? It feels confusing to have a “replace 
everything” method when one could just create something entirely new. 
Technically one could even call `$queryParams->fromArray()`, since 
calling static methods on instances is legal.

13. “doesn't start with a “with” suffix” - that should read “prefix”. 
Prefix is “start with”, suffix is “end with”.

14. The “Backward Incompatible Changes” section should mention that 
adding a new class is technically backwards incompatible, but an allowed 
break according to policy and that the Uri namespace is reserved for 
ext/uri since 8.5.

That's it. It looks like a long list of items, but most of them are 
minor concerns and suggestion. Overall I'm super happy with how the RFC 
turned out and I believe that giving it the proper attention as a 
dedicated RFC was the right choice.

Best regards
Tim Düsterhus

---

## #132874 — kocsismate90@gmail.com — 2026-10-16T12:53:57+00:00

URL: https://externals.io/message/130217#132874 · In reply to: #132851

Hi Tim,

Thanks for the detailed review! Let me share my answers inline:

1. I don't think `QueryParamParsingOptions::$maxQueryStringLength` is
> necessary or useful to have. Checking the input length is easy to do
> manually (it's just a `strlen()` call) and I don't see a practical way
> to abuse the API with large input lengths alone: The API will not “blow
> up” inputs into considerably larger data structures, the input strings
> are mostly just “copied” over, not too different from e.g.
> `str_replace()`.
>

I was also thinking a lot about whether this option is necessary to have.
My main reason for including it was that people would otherwise easily
forget
to limit the input length, whereas the dedicated option enforces the limit
everywhere.

So the real question is whether QueryParams really cannot be abused via
long strings, and whether $maxParamCount alone could mitigate those risks?

And now by thinking about it more, neither I could identify any specific
vulnerability that
would become possible by not enforcing the upper limit. In the previous
iteration
of the RFC, limiting the length was more useful, because parsing would have
been
a more resource intensive operation due to the requirements of the old
"array API".
The operations that are performed now during parsing are cheap, so as far
as I can see,
it's possible to get rid of this option if no one can come up with a good
enough use-case.


> 3. If `$maxQueryStringLength` goes away, the `QueryParamParsingOptions`
> would be a single property object. Maybe the param count limit should
> become a regular parameter then, since I can't think of any other useful
> parse-time options.
>

Yes, without trying to future proof the API too much, I guess this is the
only sensible
choice, unless someone finds any other option which would be needed.

4. For the `QueryParamBuildingOptions` I don't think they should be
> passed as parameter to the parsing functions (and thus not be stored as
> an object property). Instead they should be passed to the `->to*()`
> functions just when they are needed. That keeps the QueryParams class’
> state focused on the actual parameters.
>
> Edit after getting further down the RFC: This choice is explained
> further below, because it immediately affects the getters. This makes
> sense, but I still believe this is a less than ideal API design. But I
> don't have better proposals other than “force the user to make a
> decision and only allow passing in `string`”. For `int` the implicit
> conversion to string would just work and making an explicit choice for
> `bool` is easy enough.
>

Yes, it was another inner debate for me whether relying on explicit casting
to a string
by the user or supporting automatic conversion is a better approach. I
believe
offering some kind of automatic casting approach is more ergonomic, so I
think
it's useful to try to keep this mechanism if possible.

Moreover, the fromArray() method reads the input array recursively, which
means
that in the absence of building options, users would need to make sure that
each
parameter value is casted to a supported type, even the nested ones. For
most legal cases,
the conversion is trivial (int, float, backed enum), so why require an
explicit conversion?

On the other hand, the query param representation of boolean or null values
may vary
case by case, and as I've just realized it's not as simple as I previously
thought:

Let's take a Consul API endpoint for example (this is just a random example
which I
found first):
https://developer.hashicorp.com/consul/api-docs/operator/usage#global
The "global" query parameter represents a bool value, but only the query
parameter name needs to be passed, so an example request URL looks like
this:
"http://127.0.0.1:8500/v1/operator/usage?global"

If one would build the query parameters, probably the following code would
be used:

$params = new Uri\QueryParams();
if ($global) {
    $params = $params->set("global", null);
}

This example made me realize that neither the modification methods should
respect
the $nullPolicy (in addition to hasValue() and deleteValue()), because
otherwise
one couldn't be sure if "global" would be included in the request -- it
would be omitted
if $nullPolicy is "SkipTuple".

As a result, I'm fine with changing the input param type for append() and
set():
instead of "mixed", maybe we could accept
"string|int|float|BackedEnum|null"? I still
think that using a "?string" type would be too pedantic. Going with the
above signature
would mean that one would need to cast a bool input to any of the supported
ones (or
omit them e.g. in case of false, as seen in the example above), and they
would also need
to handle null values on a case by case basis, since passing a null value
would always
add the parameter without a value (in line with
QueryParamNullPolicy::KeyOnly),
and the behavior of QueryParamNullPolicy::SkipTuple could be emulated by
simply skipping
calling append() or set() on the input. I don't know if it makes sense to
you,
but that's the idea which currently sounds the best for me.

However, I'm still unsure in case of fromArray() what would be the correct
solution that is
also as ergonomic as possible.


> 5. Is having a separate `parseRfc3986()` useful when RFC 3986 doesn't
> specify the handling of query parameters and just delegates to RFC 1866
> instead? A big benefit of the URI extension for me is that it clearly
> references the relevant standards in its naming and strictly implements
> them, making it easy to discover and allowing the use of primary
> references to understand what they are supposed to do. The
> `parseRfc3986()` method would muddy this straight-forward standard’s
> compliant implementation, particularly since it is defined to accept
> malformed inputs such as `#foo=bar`, which I would find unexpected from
> a method referencing RFC 3986 in its name.
>

As far as I can see, supporting RFC 3986 parsing is necessary to keep full
compatibility with the URI class so that the meaning of literal +
characters in
the query string wouldn't change during query parameter parsing.
Let's consider this code:

$uri = new Uri\Rfc3986\Uri("?q=C++");

$rfc1866Params = Uri\QueryParams::parseRfc1866($uri->getQuery());
$rfc3986Params = Uri\QueryParams::parseRfc3986($uri->getQuery());

var_dump($rfc1866Params->getFirst("q"));                    // string(3)
"C  "
var_dump($rfc3986Params->getFirst("q"));                    // string(3)
"C++"

Unfortunately, WHATWG URL's URL and URLSearchParams classes have different
query parameter parsing algorithms (see the "Percent-Encoding and Decoding"
section),
so the same issue is already present there, and unavoidable. I'd like not
to repeat it in
case of RFC 3986, that's why I think parseRfc3986() is needed. But let me
know if
I miss something.

Regarding accepting malformed input: uriparser's own query parameter
handling
functions (uriDissectQueryMalloc*()) behave the same way as the RFC currently
proposes.
Unfortunately, I could not really find any reasoning behind this choice of
uriparser.

To be clear, I don't insist on following uriparser's behavior for RFC 1866
and RFC 3986 (it's
not an option for WHATWG URL, since it specifies what needs to be done in
every detail),
and probably strict parsing is what is more consistent with the Uri class
indeed.

Then passing an invalid query string syntax would throw. However, incorrect
UTF-8 sequences
would still be allowed, since neither RFC 1866, nor RFC 3986 require a
syntactically
correct query string to be valid UTF-8.

7. I cannot describe an actual issue, but I find accepting invalid
> percent-encoding sequences to be dangerous. It would definitely be
> unexpected that `%6f=x' would be `o=x`, but `%6g=y` would be `%6g=y`
> (which would then become `%256g=y`).
>

Answered above. :)

8. The naming of `hasValue()` sounds like it would check if there is a
> `=` sign for the given key somewhere, not that it would check that there
> is a specific key-value pair. I'm also not sure if it is necessary.
> Duplicated keys are comparatively rare and for the use cases where this
> is a thing, `in_array()` on the result of `getAll()` would just work (or
> `array_any()` for more complex checks).
>

This method is required by WHATWG URL:
https://url.spec.whatwg.org/#dom-urlsearchparams-has
It's specified as an overloaded method, so I had to introduce a separate
signature for this use-case, as per the policy accepted in
https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#policy_about_new_functions#policy_about_new_functions
.

TBH, I find it useful (more convenient and more efficient than getAll() +
 `in_array()`).
The name itself may be slightly confusing at the first sight, but I think
the
$value parameter which must be passed should clarify its purpose. But in
any case,
I'm open to improving its name.


> 9. I'm not sure I like the name `->list()`. Technically it's also
> redundant with `iterator_to_array($queryParams)`, but having a dedicated
> method probably makes sense nevertheless.
>

Yes, I'm fine with renaming this method to toList(). It would go well with
toArray()
IMO. It's more ergonomic and more efficient than the alternative, so I agree
that it's useful.

10. Would it make sense to rename `parseWhatWg()` to
> `parseUrlSearchParams()` or similar to make it clear that this
> constructor implements the URLSearchParams specification. I'm asking due
> to the section that mentioned how `$whatWgUrl->getQuery()` is not the
> correct value to pass there (due to the questionmark handling). Renaming
> this method would also make sense when removing the RFC 3986 parser (as
> suggested in (5)).
>

I think it's a good idea. Maybe people using it would be less likely to be
surprised by
its behavior (how it handles the leading ? character) if the name referred
to
URLSearchParams. However, I think including WHATWG in the method name
would still have its purpose (people with less familiarity with WHATWG URL
would know immediately that this method is related to the WHATWG URL
specification),
so my preferred name would be parseWhatWgUrlSearchParams(), even
if it's somewhat verbose.

11. Bullet point 8 also applies to `deleteValue()`. Is there an actual
> use case for this method?
>

Yes, it has the same reason:
https://url.spec.whatwg.org/#dom-urlsearchparams-delete

Also, this function may come handy in case of filtering. For example, let's
consider the
"tag=php&tag=web&sort=date" query string: deleteValue("tag", "php") would
remove one
selected filter while keeping the other parameters intact.


> 12. Is having the `withArray()` method useful? It does the same as
> `QueryParams::fromArray()`, no? It feels confusing to have a “replace
> everything” method when one could just create something entirely new.
> Technically one could even call `$queryParams->fromArray()`, since
> calling static methods on instances is legal.
>

Currently, the only description about this method is the following:

It replaces the whole query list with the input, but clones the current
> instance rather than creating a completely new one.


So it's a more efficient form of instantiating a class based on an array
input than fromArray().
Additionally, it was designed to also keep the original building options
(if they are going to be kept
in the final proposal).

$queryParams = Uri\QueryParams::parseRfc3986("foo=bar", 1000, new
Uri\QueryParamBuildingOptions("true", "false"));
$array = [...];
// Currently preferred solution:
$queryParams = $queryParams->withArray($array);
// The same solution using fromArray():
$queryParams = Uri\QueryParams::fromArray($array,
$queryParams->getBuildingOptions());

P.S. The getBuildingOptions() doesn't exist yet in the proposal, and it
should be added if building options remain in the RFC at last.

13. “doesn't start with a “with” suffix” - that should read “prefix”.
> Prefix is “start with”, suffix is “end with”.
>

 Yeah, thanks for noticing it. It was an unfortunate typo, and I had
already fixed it in my draft version when you were reading it.

14. The “Backward Incompatible Changes” section should mention that
> adding a new class is technically backwards incompatible, but an allowed
> break according to policy and that the Uri namespace is reserved for
> ext/uri since 8.5.
>

Fixed.

One more thing which I'm probably going to change: it turned out that
WHATWG URL specifies
the `sort()` method (https://url.spec.whatwg.org/#dom-urlsearchparams-sort)
which must use UTF-16 based
comparisons (using 2 bytes). It is really not obvious from the WHATWG URL,
only the WHATWG Infra
specification defines it: https://infra.spec.whatwg.org/#code-unit-less-than

In order to restore compatibility with this requirement, I'd define two
sorting algorithms:
- sortByBytes(): the currently specified one which would use bytes as the
base of comparison
- sortByUtf16(): the one comparing UTF-16 sequences at once which would be
compatible with WHATWG URL

Again, thank you for the review!

Máté
