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.
2.3 KiB
fix: keep valueless query parameters when casting to a string
Problem
A query parameter that has no value (for example ?foo or ?foo=) is parsed and
stored, and has() / getAllQueryParameters() report it as present, but casting
the bag (or the Url) back to a string silently drops it:
$url = Spatie\Url\Url::fromString('https://example.com/?foo=');
$url->hasQueryParameter('foo'); // true
(string) $url; // "https://example.com" <-- "foo" is gone
So hasQueryParameter('foo') is true while the parameter disappears from the
string representation, and the URL does not round-trip.
Root cause
src/QueryParameterBag.php. fromString() deliberately stores valueless
parameters as null (added in #11, and pinned by the "empty query parameters
default to null" test):
$parameters = array_map(fn ($param) => $param !== '' ? $param : null, $parameters);
but __toString() passes those null values straight to http_build_query():
return http_build_query($this->parameters, '', '&', PHP_QUERY_RFC3986);
http_build_query() skips null values entirely, so every valueless parameter
is removed from the output.
The fix
Render null values as empty-valued parameters before building the query string,
so they survive the round-trip:
$parameters = array_map(fn ($value) => $value ?? '', $this->parameters);
return http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
?foo / ?foo= now serialize back to foo= (which parses to the same value),
and parameters with an actual value or an array value are unaffected.
Tests
Added two cases to QueryParameterBagTest:
it keeps valueless parameters when being casted to a string(fromString('offset')->has('offset')true and__toString()===offset=).it keeps valueless parameters alongside valued ones when being casted to a string(fromString('foo=&bar=baz')->__toString()===foo=&bar=baz).
Gate
composer install && vendor/bin/pest
(phpunit is intentionally blocked by this package in favour of Pest.)
Result: exit code 0 - Tests: 110 passed.
Counterfactual: reverting __toString() to pass $this->parameters directly to
http_build_query() makes both new tests fail (the valueless parameters are
dropped from the output).