fix: keep valueless query parameters when casting to a string

Valueless query parameters (for example ?foo) are stored as null, and has()
reports them as present, but __toString() passed those null values to
http_build_query() which skips them entirely, dropping the parameters from the
output. Render null values as empty-valued parameters so they survive a
round-trip.
This commit is contained in:
dualfroz committed 2026-09-06 00:59:19 +02:00
1 parent 53eebaebee
commit ef1f80eb18
2 files changed
+20 -1

No files matched your search

+5 -1
View File
@@ -65,6 +65,10 @@ class QueryParameterBag implements \Stringable
public function __toString(): string public function __toString(): string
{ {
return http_build_query($this->parameters, '', '&', PHP_QUERY_RFC3986); // Valueless parameters are stored as null, which http_build_query() skips;
// render them with an empty value so they survive.
$parameters = array_map(fn ($value) => $value ?? '', $this->parameters);
return http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
} }
} }
+15
View File
@@ -89,6 +89,21 @@ it('can be casted to a string', function () {
}); });
it('keeps valueless parameters when being casted to a string', function () {
$queryParameterBag = QueryParameterBag::fromString('offset');
expect($queryParameterBag)->has('offset')->toBeTrue();
expect($queryParameterBag)->__toString()->toEqual('offset=');
});
it('keeps valueless parameters alongside valued ones when being casted to a string', function () {
$queryParameterBag = QueryParameterBag::fromString('foo=&bar=baz');
expect($queryParameterBag)->__toString()->toEqual('foo=&bar=baz');
});
it('can be created from a string with url encoded values', function () { it('can be created from a string with url encoded values', function () {
$queryParameterBag = QueryParameterBag::fromString( $queryParameterBag = QueryParameterBag::fromString(
'category=storage%20furniture&discount=%3E40%25%20off&range%5B0%5D=10&range%5B1%5D=20' 'category=storage%20furniture&discount=%3E40%25%20off&range%5B0%5D=10&range%5B1%5D=20'