Skip to content

Commit

Permalink
Bugfix/query parameters constructors encoding values (#122)
Browse files Browse the repository at this point in the history
bugfix inconsistencies when converting variables into query
  • Loading branch information
nyamsprod authored Sep 29, 2023
1 parent 9264fba commit 84b66f0
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 47 deletions.
6 changes: 4 additions & 2 deletions components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ All Notable changes to `League\Uri\Components` will be documented in this file

### Added

- None
- `Query::fromVariable`
- `UrlSearchParams::fromVariable`

### Fixed

- None

### Deprecated

- None
- `Query::fromParameters` use `Query::fromVariable` instead
- `UrlSearchParams::fromParameters` use `UrlSearchParams::fromVariable` instead

### Removed

Expand Down
53 changes: 35 additions & 18 deletions components/Components/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use League\Uri\QueryString;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;

use Traversable;

use function array_column;
Expand All @@ -35,7 +36,6 @@
use function http_build_query;
use function implode;
use function is_int;
use function iterator_to_array;
use function preg_match;
use function preg_quote;
use function preg_replace;
Expand Down Expand Up @@ -69,27 +69,13 @@ public static function new(Stringable|string|null $value = null): self
}

/**
* Returns a new instance from the result of PHP's parse_str.
* Returns a new instance from the input of http_build_query.
*
* @param non-empty-string $separator
*/
public static function fromParameters(object|array $parameters, string $separator = '&'): self
public static function fromVariable(object|array $parameters, string $separator = '&'): self
{
if ($parameters instanceof QueryInterface) {
return self::fromPairs($parameters, $separator);
}

$parameters = match (true) {
$parameters instanceof Traversable => iterator_to_array($parameters),
default => $parameters,
};

$query = match ([]) {
$parameters => null,
default => http_build_query(data: $parameters, arg_separator: $separator),
};

return new self($query, Converter::fromRFC1738($separator));
return new self(http_build_query(data: $parameters, arg_separator: $separator), Converter::fromRFC1738($separator));
}

/**
Expand Down Expand Up @@ -668,4 +654,35 @@ public function withoutPair(string ...$keys): QueryInterface
{
return $this->withoutPairByKey(...$keys);
}

/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @param non-empty-string $separator
*
* @see Query::fromVariable()
*
* @codeCoverageIgnore
* Returns a new instance from the result of PHP's parse_str.
*
* @deprecated Since version 7.0.0
*/
public static function fromParameters(object|array $parameters, string $separator = '&'): self
{
if ($parameters instanceof QueryInterface) {
return self::fromPairs($parameters, $separator);
}

$parameters = match (true) {
$parameters instanceof Traversable => iterator_to_array($parameters),
default => $parameters,
};

$query = match ([]) {
$parameters => null,
default => http_build_query(data: $parameters, arg_separator: $separator),
};

return new self($query, Converter::fromRFC1738($separator));
}
}
32 changes: 20 additions & 12 deletions components/Components/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ public static function providePairsToBeRemoved(): iterable
*/
public function testwithoutParam(array $origin, array $without, string $expected): void
{
self::assertSame($expected, Query::fromParameters($origin)->withoutParameters(...$without)->toString());
self::assertSame($expected, Query::fromVariable($origin)->withoutParameters(...$without)->toString());
}

public static function withoutParamProvider(): array
Expand Down Expand Up @@ -408,7 +408,7 @@ public function testWithoutParamDoesNotChangeParamsKey(): void
],
];

$query = Query::fromParameters($data);
$query = Query::fromVariable($data);
self::assertSame('foo%5B0%5D=bar&foo%5B1%5D=baz', $query->value());

self::assertTrue($query->hasParameter('foo'));
Expand Down Expand Up @@ -437,13 +437,13 @@ public function testCreateFromParamsWithTraversable(): void
],
];

self::assertSame($data, Query::fromParameters(new ArrayIterator($data))->parameters());
self::assertSame([], Query::fromVariable(new ArrayIterator($data))->parameters());
}

public function testCreateFromParamsWithQueryObject(): void
{
$query = Query::new('a=1&b=2');
self::assertEquals($query->value(), Query::fromParameters($query)->value());
self::assertEquals('pairs%5B0%5D%5B0%5D=a&pairs%5B0%5D%5B1%5D=1&pairs%5B1%5D%5B0%5D=b&pairs%5B1%5D%5B1%5D=2&separator=%26&parameters%5Ba%5D=1&parameters%5Bb%5D=2', Query::fromVariable($query)->value());
}

public static function testWithoutNumericIndices(): void
Expand All @@ -464,7 +464,7 @@ public static function testWithoutNumericIndices(): void
$withIndices = 'filter%5Bfoo%5D%5B0%5D=bar&filter%5Bfoo%5D%5B1%5D=baz&filter%5Bbar%5D%5Bbar%5D=foo&filter%5Bbar%5D%5Bfoo%5D=bar';
$withoutIndices = 'filter%5Bfoo%5D%5B%5D=bar&filter%5Bfoo%5D%5B%5D=baz&filter%5Bbar%5D%5Bbar%5D=foo&filter%5Bbar%5D%5Bfoo%5D=bar';

$query = Query::fromParameters($data);
$query = Query::fromVariable($data);
self::assertSame($withIndices, $query->value());
self::assertSame($data, $query->parameters());

Expand All @@ -488,43 +488,43 @@ public function testWithoutNumericIndicesReturnsAnother(): void

public function testWithoutNumericIndicesDoesNotAffectPairValue(): void
{
$query = Query::fromParameters(['foo' => 'bar[3]']);
$query = Query::fromVariable(['foo' => 'bar[3]']);

self::assertSame($query, $query->withoutNumericIndices());
}

public function testCreateFromParamsOnEmptyParams(): void
{
$query = Query::fromParameters([]);
$query = Query::fromVariable([]);

self::assertSame($query, $query->withoutNumericIndices());
}

public function testGetContentOnEmptyContent(): void
{
self::assertNull(Query::fromParameters([])->value());
self::assertSame('', Query::fromVariable([])->value());
}

public function testGetContentOnHavingContent(): void
{
self::assertSame('foo=bar', Query::fromParameters(['foo' => 'bar'])->value());
self::assertSame('foo=bar', Query::fromVariable(['foo' => 'bar'])->value());
}

public function testGetContentOnToString(): void
{
self::assertSame('foo=bar', (string) Query::fromParameters(['foo' => 'bar']));
self::assertSame('foo=bar', (string) Query::fromVariable(['foo' => 'bar']));
}

public function testWithSeperatorOnHavingSeparator(): void
{
$query = Query::fromParameters(['foo' => '/bar']);
$query = Query::fromVariable(['foo' => '/bar']);

self::assertSame($query, $query->withSeparator('&'));
}

public function testWithoutNumericIndicesOnEmptyContent(): void
{
$query = Query::fromParameters([]);
$query = Query::fromVariable([]);

self::assertSame($query, $query->withoutNumericIndices());
}
Expand Down Expand Up @@ -800,4 +800,12 @@ public function testItFailsToCreateFromRFCSpecificationWithEmptySeparator(): voi

Query::fromRFC1738('foo=b%20ar;foo=baz', ''); /* @phpstan-ignore-line */
}

public function testInstantiationFromURLSearchParams(): void
{
$expected = ['foo' => 'bar'];
$query = Query::fromVariable(URLSearchParams::fromVariable($expected));

self::assertSame('', $query->value());
}
}
64 changes: 60 additions & 4 deletions components/Components/URLSearchParams.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,20 @@
use Stringable;

use function array_is_list;
use function array_key_exists;
use function array_keys;
use function array_map;
use function count;
use function func_get_arg;
use function func_num_args;
use function get_object_vars;
use function is_array;
use function is_iterable;
use function is_object;
use function is_scalar;
use function iterator_to_array;
use function json_encode;
use function spl_object_hash;
use function str_starts_with;

use const JSON_PRESERVE_ZERO_FRACTION;
Expand Down Expand Up @@ -206,11 +211,48 @@ public static function fromUri(Stringable|string $uri): self
}

/**
* Returns a new instance from the result of PHP's parse_str.
* Returns a new instance from the input of PHP's http_build_query.
*/
public static function fromParameters(object|array $parameters): self
public static function fromVariable(object|array $parameters): self
{
return new self(Query::fromParameters($parameters));
return self::fromPairs(self::parametersToPairs($parameters));
}

private static function parametersToPairs(array|object $data, string|int $prefix = '', array &$recursive = []): array
{
$yieldParameters = static fn (object|array $data): array => is_array($data) ? $data : get_object_vars($data);

$pairs = [];
foreach ($yieldParameters($data) as $name => $value) {
if (is_object($data)) {
$id = spl_object_hash($data);
if (!array_key_exists($id, $recursive)) {
$recursive[$id] = 1;
}
}

if (is_object($value)) {
$id = spl_object_hash($value);
if (array_key_exists($id, $recursive)) {
return [];
}

$recursive[$id] = 1;
}

if ('' !== $prefix) {
$name = $prefix.'['.$name.']';
}

$pairs = match (true) {
is_array($value),
is_object($value) => [...$pairs, ...self::parametersToPairs($value, $name, $recursive)],
is_scalar($value) => [...$pairs, [$name, self::uvString($value)]],
default => $pairs,
};
}

return $pairs;
}

public function value(): ?string
Expand Down Expand Up @@ -424,7 +466,7 @@ public function delete(?string $name): void
$this->updateQuery(match (func_num_args()) {
1 => $this->pairs->withoutPairByKey($name),
2 => $this->pairs->withoutPairByKeyValue($name, self::uvString(func_get_arg(1))), /* @phpstan-ignore-line */
default => throw new ArgumentCountError(__METHOD__.' requires at least one r as the pair name and a second optional argument as the pair value.'),
default => throw new ArgumentCountError(__METHOD__.' requires at least one argument as the pair name and a second optional argument as the pair value.'),
});
}

Expand All @@ -439,4 +481,18 @@ public function sort(): void
{
$this->updateQuery($this->pairs->sort());
}

/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.4.0
* @see URLSearchParams::fromVariable()
*
* @codeCoverageIgnore
*
*/
public static function fromParameters(object|array $parameters): self
{
return new self(Query::fromParameters($parameters));
}
}
Loading

0 comments on commit 84b66f0

Please sign in to comment.