Re: [RFC] [Discussion] Query Parameter Manipulation Support
| From: | ignace nyamagana butera | Date: | Tue, 14 Jul 2026 11:14:44 +0000 |
| Subject: | Re: [RFC] [Discussion] Query Parameter Manipulation Support | ||
| References: | 1 2 3 4 5 6 7 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-131915@lists.php.net to get a copy of this message | ||
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:
>
>> 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 theto* 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