Files
dualfroz d71aedae66 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.
2026-09-04 09:37:47 +02:00

73 lines
2.3 KiB
Markdown

# 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:
```php
$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):
```php
$parameters = array_map(fn ($param) => $param !== '' ? $param : null, $parameters);
```
but `__toString()` passes those `null` values straight to `http_build_query()`:
```php
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:
```php
$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).