* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Uri; use ArrayIterator; use League\Uri\Components\Fragment; use League\Uri\Exceptions\SyntaxError; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use stdClass; use Stringable; use TypeError; use ValueError; use function date_create; use function http_build_query; use function tmpfile; use const PHP_QUERY_RFC1738; use const PHP_QUERY_RFC3986; use const PHP_VERSION_ID; final class QueryStringTest extends TestCase { public function testEncodingThrowsExceptionWithQueryParser(): void { $this->expectException(SyntaxError::class); QueryString::parse('foo=bar', '&', 42); } public function testSyntaxErrorThrowsExceptionWithQueryParser(): void { $this->expectException(SyntaxError::class); QueryString::parse("foo=bar\0"); } public function testSyntaxErrorThrowsExceptionWithQueryParserAndAnEmptySeparator(): void { $this->expectException(SyntaxError::class); QueryString::parse('foo=bar', ''); /* @phpstan-ignore-line */ } public function testEncodingThrowsExceptionWithQueryBuilder(): void { $this->expectException(SyntaxError::class); QueryString::build([['foo', 'bar']], '&', 42); } public function testBuildThrowsExceptionWithQueryBuilder(): void { $this->expectException(SyntaxError::class); QueryString::build([['foo', 'boo' => 'bar']]); /* @phpstan-ignore-line */ } #[DataProvider('extractQueryProvider')] public function testExtractQuery(Stringable|string|null|bool $query, array $expected): void { self::assertSame($expected, QueryString::extract($query)); } public static function extractQueryProvider(): array { return [ [ 'query' => null, 'expected' => [], ], [ 'query' => false, 'expected' => ['0' => ''], ], [ 'query' => '%25car=%25car', 'expected' => ['%car' => '%car'], ], [ 'query' => '&&', 'expected' => [], ], [ 'query' => true, 'expected' => ['1' => ''], ], [ 'query' => false, 'expected' => ['0' => ''], ], [ 'query' => 'arr[1=sid&arr[4][2=fred', 'expected' => [ 'arr[1' => 'sid', 'arr' => ['4' => 'fred'], ], ], [ 'query' => 'arr1]=sid&arr[4]2]=fred', 'expected' => [ 'arr1]' => 'sid', 'arr' => ['4' => 'fred'], ], ], [ 'query' => 'arr[one=sid&arr[4][two=fred', 'expected' => [ 'arr[one' => 'sid', 'arr' => ['4' => 'fred'], ], ], [ 'query' => 'first=%41&second=%a&third=%b', 'expected' => [ 'first' => 'A', 'second' => '%a', 'third' => '%b', ], ], [ 'query' => 'arr.test[1]=sid&arr test[4][two]=fred', 'expected' => [ 'arr.test' => ['1' => 'sid'], 'arr test' => ['4' => ['two' => 'fred']], ], ], [ 'query' => 'foo&bar=&baz=bar&fo.o', 'expected' => [ 'foo' => '', 'bar' => '', 'baz' => 'bar', 'fo.o' => '', ], ], [ 'query' => 'foo[]=bar&foo[]=baz', 'expected' => [ 'foo' => ['bar', 'baz'], ], ], ]; } /** * @param non-empty-string $separator */ #[DataProvider('parserProvider')] public function testParse(Stringable|string|null|bool $query, string $separator, array $expected, int $encoding): void { self::assertSame($expected, QueryString::parse($query, $separator, $encoding)); } public static function parserProvider(): array { return [ 'URI Component Object object' => [ Fragment::new('a=1&a=2'), '&', [['a', '1'], ['a', '2']], PHP_QUERY_RFC3986, ], 'stringable object' => [ new class () { public function __toString(): string { return 'a=1&a=2'; } }, '&', [['a', '1'], ['a', '2']], PHP_QUERY_RFC3986, ], 'rfc1738 without hexaencoding' => [ 'to+to=foo%2bbar', '&', [['to to', 'foo+bar']], PHP_QUERY_RFC1738, ], 'null value' => [ null, '&', [], PHP_QUERY_RFC3986, ], 'empty string' => [ '', '&', [['', null]], PHP_QUERY_RFC3986, ], 'bool value' => [ false, '&', [['0', null]], PHP_QUERY_RFC1738, ], 'identical keys' => [ 'a=1&a=2', '&', [['a', '1'], ['a', '2']], PHP_QUERY_RFC3986, ], 'no value' => [ 'a&b', '&', [['a', null], ['b', null]], PHP_QUERY_RFC3986, ], 'empty value' => [ 'a=&b=', '&', [['a', ''], ['b', '']], PHP_QUERY_RFC3986, ], 'php array' => [ 'a[]=1&a[]=2', '&', [['a[]', '1'], ['a[]', '2']], PHP_QUERY_RFC3986, ], 'preserve dot' => [ 'a.b=3', '&', [['a.b', '3']], PHP_QUERY_RFC3986, ], 'decode' => [ 'a%20b=c%20d', '&', [['a b', 'c d']], PHP_QUERY_RFC3986, ], 'no key stripping' => [ 'a=&b', '&', [['a', ''], ['b', null]], PHP_QUERY_RFC3986, ], 'no value stripping' => [ 'a=b=', '&', [['a', 'b=']], PHP_QUERY_RFC3986, ], 'key only' => [ 'a', '&', [['a', null]], PHP_QUERY_RFC3986, ], 'preserve falsey 1' => [ '0', '&', [['0', null]], PHP_QUERY_RFC3986, ], 'preserve falsey 2' => [ '0=', '&', [['0', '']], PHP_QUERY_RFC3986, ], 'preserve falsey 3' => [ 'a=0', '&', [['a', '0']], PHP_QUERY_RFC3986, ], 'different separator' => [ 'a=0;b=0&c=4', ';', [['a', '0'], ['b', '0&c=4']], PHP_QUERY_RFC3986, ], 'numeric key only' => [ '42', '&', [['42', null]], PHP_QUERY_RFC3986, ], 'numeric key' => [ '42=l33t', '&', [['42', 'l33t']], PHP_QUERY_RFC3986, ], 'rfc1738' => [ '42=l3+3t', '&', [['42', 'l3 3t']], PHP_QUERY_RFC1738, ], ]; } #[DataProvider('buildProvider')] public function testBuild( iterable $pairs, ?string $expected_rfc1738, ?string $expected_rfc3986 ): void { self::assertSame($expected_rfc1738, QueryString::build($pairs, '&', PHP_QUERY_RFC1738)); self::assertSame($expected_rfc3986, QueryString::build($pairs, '&', PHP_QUERY_RFC3986)); } public static function buildProvider(): array { return [ 'empty string' => [ 'pairs' => [], 'expected_rfc1738' => null, 'expected_rfc3986' => null, ], 'identical keys' => [ 'pairs' => new ArrayIterator([['a', true] , [true, 'a']]), 'expected_rfc1738' => 'a=1&1=a', 'expected_rfc3986' => 'a=1&1=a', ], 'no value' => [ 'pairs' => [['a', null], ['b', null]], 'expected_rfc1738' => 'a&b', 'expected_rfc3986' => 'a&b', ], 'empty value' => [ 'pairs' => [['a', ''], ['b', 1.3]], 'expected_rfc1738' => 'a=&b=1.3', 'expected_rfc3986' => 'a=&b=1.3', ], 'php array (1)' => [ 'pairs' => [['a[]', '1%a6'], ['a[]', '2']], 'expected_rfc1738' => 'a%5B%5D=1%25a6&a%5B%5D=2', 'expected_rfc3986' => 'a%5B%5D=1%25a6&a%5B%5D=2', ], 'php array (2)' => [ 'pairs' => [['module', 'home'], ['action', 'show'], ['page', '😓']], 'expected_rfc1738' => 'module=home&action=show&page=%F0%9F%98%93', 'expected_rfc3986' => 'module=home&action=show&page=%F0%9F%98%93', ], 'php array (3)' => [ 'pairs' => [['module', 'home'], ['action', 'v%61lue']], 'expected_rfc1738' => 'module=home&action=v%2561lue', 'expected_rfc3986' => 'module=home&action=v%2561lue', ], 'preserve dot' => [ 'pairs' => [['a.b', '3']], 'expected_rfc1738' => 'a.b=3', 'expected_rfc3986' => 'a.b=3', ], 'no key stripping' => [ 'pairs' => [['a', ''], ['b', null]], 'expected_rfc1738' => 'a=&b', 'expected_rfc3986' => 'a=&b', ], 'no value stripping' => [ 'pairs' => [['a', 'b=']], 'expected_rfc1738' => 'a=b%3D', 'expected_rfc3986' => 'a=b%3D', ], 'key only' => [ 'pairs' => [['a', null]], 'expected_rfc1738' => 'a', 'expected_rfc3986' => 'a', ], 'preserve falsey 1' => [ 'pairs' => [['0', null]], 'expected_rfc1738' => '0', 'expected_rfc3986' => '0', ], 'preserve falsey 2' => [ 'pairs' => [['0', '']], 'expected_rfc1738' => '0=', 'expected_rfc3986' => '0=', ], 'preserve falsey 3' => [ 'pairs' => [['0', '0']], 'expected_rfc1738' => '0=0', 'expected_rfc3986' => '0=0', ], 'rcf1738' => [ 'pairs' => [['toto', 'foo+bar toto']], 'expected_rfc1738' => 'toto=foo%2Bbar+toto', 'expected_rfc3986' => 'toto=foo%2Bbar%20toto', ], 'utf-8 characters' => [ 'pairs' => [["\v\xED", "\v\xED"]], 'expected_rfc1738' => '%0B%ED=%0B%ED', 'expected_rfc3986' => '%0B%ED=%0B%ED', ], 'uri in value' => [ 'pairs' => [['url', 'https://uri.thephpleague.com/components/2.0/?module=home#what-you-will-be-able-to-do with space']], 'expected_rfc1738' => 'url=https%3A%2F%2Furi.thephpleague.com%2Fcomponents%2F2.0%2F%3Fmodule%3Dhome%23what-you-will-be-able-to-do+with+space', 'expected_rfc3986' => 'url=https%3A%2F%2Furi.thephpleague.com%2Fcomponents%2F2.0%2F%3Fmodule%3Dhome%23what-you-will-be-able-to-do%20with%20space', ], ]; } #[DataProvider('failedBuilderProvider')] public function testBuildQueryThrowsException(iterable $pairs, string $separator, int $enc_type): void { $this->expectException(SyntaxError::class); QueryString::build($pairs, $separator, $enc_type); /* @phpstan-ignore-line */ } public static function failedBuilderProvider(): array { return [ 'The collection cannot contain empty pair' => [ [[]], '&', PHP_QUERY_RFC1738, ], 'The pair key must be stringable' => [ [[date_create(), 'bar']], '&', PHP_QUERY_RFC1738, ], 'The pair value must be stringable or null - rfc3986/rfc1738' => [ [['foo', date_create()]], '&', PHP_QUERY_RFC3986, ], 'identical keys with associative array' => [ new ArrayIterator([['key' => 'a', 'value' => true] , ['key' => 'a', 'value' => '2']]), '&', PHP_QUERY_RFC3986, ], 'Object' => [ [['a[]', (object) '1']], '&', PHP_QUERY_RFC1738, ], 'the separator cannot be the empty string' => [ [['foo', 'bar']], '', PHP_QUERY_RFC3986, ], ]; } #[DataProvider('queryProvider')] public function testStringRepresentationComponent(string|array $input, string|null $expected): void { $query = is_array($input) ? QueryString::build($input) : QueryString::build(QueryString::parse($input)); self::assertSame($expected, $query); } public static function queryProvider(): array { $unreserved = 'a-zA-Z0-9.-_~!$&\'()*,;=:@'; return [ 'bug fix issue 84' => ['fào=?%25bar&q=v%61lue', 'f%C3%A0o=%3F%25bar&q=value'], 'string' => ['kingkong=toto', 'kingkong=toto'], 'query object' => ['kingkong=toto', 'kingkong=toto'], 'empty string' => ['', ''], 'empty array' => [[], null], 'non empty array' => [[['', null]], ''], 'contains a reserved word #' => ['foo%23bar', 'foo%23bar'], 'contains a delimiter ?' => ['?foo%23bar', '%3Ffoo%23bar'], 'key-only' => ['k^ey', 'k%5Eey'], 'key-value' => ['k^ey=valu`', 'k%5Eey=valu%60'], 'array-key-only' => ['key[]', 'key%5B%5D'], 'array-key-value' => ['key[]=valu`', 'key%5B%5D=valu%60'], 'complex' => ['k^ey&key[]=valu`&f<>=`bar', 'k%5Eey&key%5B%5D=valu%60&f%3C%3E=%60bar'], 'Percent encode spaces' => ['q=va lue', 'q=va%20lue'], 'Percent encode multibyte' => ['€', '%E2%82%AC'], "Don't encode something that's already encoded" => ['q=va%20lue', 'q=va%20lue'], 'Percent encode invalid percent encodings' => ['q=va%2-lue', 'q=va%252-lue'], "Don't encode path segments" => ['q=va/lue', 'q=va%2Flue'], "Don't encode unreserved chars or sub-delimiters" => [$unreserved, 'a-zA-Z0-9.-_~%21%24&%27%28%29%2A%2C%3B=%3A%40'], 'Encoded unreserved chars are not decoded' => ['q=v%61lue', 'q=value'], ]; } #[Test] #[DataProvider('queryWithInnerEmptyBracetsProvider')] public function it_should_parse_empty_bracets_issue_146(string $query, string $expected): void { $data = QueryString::extract($query); parse_str($query, $result); self::assertSame($data, $result); self::assertSame($expected, http_build_query($data, '', '&', PHP_QUERY_RFC3986)); } public static function queryWithInnerEmptyBracetsProvider(): iterable { yield 'query with on level empty bracets' => [ 'query' => 'foo[]=bar', 'expected' => 'foo%5B0%5D=bar', ]; yield 'query with two level bracets' => [ 'query' => 'key[][][foo][9]=bar', 'expected' => 'key%5B0%5D%5B0%5D%5Bfoo%5D%5B9%5D=bar', ]; yield 'query with invalid remaining; close bracet without an opening bracet' => [ 'query' => 'key[][]foo][9]=bar', 'expected' => 'key%5B0%5D%5B0%5D=bar', ]; yield 'query with invalid remaining; no opening bracet' => [ 'query' => 'key[]9=bar', 'expected' => 'key%5B0%5D=bar', ]; yield 'query with invalid remaining; opening bracet no at the start of the remaining string' => [ 'query' => 'key[]9[]=bar', 'expected' => 'key%5B0%5D=bar', ]; } /** * @param non-empty-string $separator */ #[DataProvider('providesVariablesToCompose')] public function test_it_can_compose_a_query_string( object|array $variable, string $separator, int $encoding, ?string $expected, QueryComposeMode $mode, ): void { self::assertSame($expected, QueryString::compose($variable, $separator, $encoding, $mode)); } public static function providesVariablesToCompose(): iterable { yield 'empty string if the variable is empty' => [ 'variable' => [], 'separator' => '&', 'encoding' => PHP_QUERY_RFC1738, 'expected' => '', 'mode' => QueryComposeMode::Native, ]; yield 'null if the variable is empty' => [ 'variable' => [], 'separator' => '&', 'encoding' => PHP_QUERY_RFC1738, 'expected' => null, 'mode' => QueryComposeMode::Safe, ]; yield 'null if the object properties are not accessible' => [ 'variable' => new stdClass(), 'separator' => '&', 'encoding' => PHP_QUERY_RFC1738, 'expected' => '', 'mode' => QueryComposeMode::Native, ]; yield 'basic encoding from php-src tests 1' => [ 'variable' => ['foo' => 'bar', 'baz' => 1, 'test' => "a ' \" ", 'abc', 'float' => 10.42, 'true' => true, 'false' => false], 'separator' => '&', 'encoding' => PHP_QUERY_RFC1738, 'expected' => 'foo=bar&baz=1&test=a+%27+%22+&0=abc&float=10.42&true=1&false=0', 'mode' => QueryComposeMode::Native, ]; yield 'basic encoding from php-src tests 2 - with a different separator' => [ 'variable' => ['foo' => 'bar', 'baz' => 1, 'test' => "a ' \" ", 'abc', 'float' => 10.42, 'true' => true, 'false' => false], 'separator' => ';', 'encoding' => PHP_QUERY_RFC1738, 'expected' => 'foo=bar;baz=1;test=a+%27+%22+;0=abc;float=10.42;true=1;false=0', 'mode' => QueryComposeMode::Native, ]; $data = new class () implements Stringable { public string $public = 'input'; protected string $protected = 'hello'; private string $private = 'world'; public function __toString(): string { return $this->private; } }; yield 'basic encoding from php-src tests 3 - with object' => [ 'variable' => $data, 'separator' => '&', 'encoding' => PHP_QUERY_RFC1738, 'expected' => 'public=input', 'mode' => QueryComposeMode::Native, ]; yield 'basic encoding from php-src tests 3 - with null object' => [ 'variable' => new stdClass(), 'separator' => '&', 'encoding' => PHP_QUERY_RFC1738, 'expected' => '', 'mode' => QueryComposeMode::Native, ]; $data = new class () implements Stringable { public function __toString(): string { return 'Stringable'; } }; yield 'basic encoding from php-src tests 4 - with stringable object without public property' => [ 'variable' => ['hello', $data], 'separator' => '&', 'encoding' => PHP_QUERY_RFC1738, 'expected' => '0=hello', 'mode' => QueryComposeMode::Native, ]; yield 'basic encoding from php-src tests 5 - with stringable object without public property' => [ 'variable' => $data, 'separator' => '&', 'encoding' => PHP_QUERY_RFC1738, 'expected' => '', 'mode' => QueryComposeMode::Native, ]; $o = new class () { public mixed $public = 'input'; }; $nested = clone $o; $o->public = $nested; yield 'basic encoding from php-src tests 6 - nested object' => [ 'variable' => $o, 'separator' => '&', 'encoding' => PHP_QUERY_RFC1738, 'expected' => 'public%5Bpublic%5D=input', 'mode' => QueryComposeMode::Native, ]; $obj = new stdClass(); $obj->name = 'homepage'; $obj->page = 1; $obj->sort = 'desc,name'; yield 'basic encoding from php-src tests 7 - stdClass' => [ 'variable' => $obj, 'separator' => '&', 'encoding' => PHP_QUERY_RFC1738, 'expected' => 'name=homepage&page=1&sort=desc%2Cname', 'mode' => QueryComposeMode::Native, ]; yield 'basic encoding from php-src tests 8 - array' => [ 'variable' => [ 20, 5 => 13, '9' => [ 1 => 'val1', 3 => 'val2', 'string' => 'string', ], 'name' => 'homepage', 'page' => 10, 'sort' => [ 'desc', 'admin' => [ 'admin1', 'admin2' => [ 'who' => 'admin2', 2 => 'test', ], ], ], ], 'separator' => '&', 'encoding' => PHP_QUERY_RFC1738, 'expected' => '0=20&5=13&9%5B1%5D=val1&9%5B3%5D=val2&9%5Bstring%5D=string&name=homepage&page=10&sort%5B0%5D=desc&sort%5Badmin%5D%5B0%5D=admin1&sort%5Badmin%5D%5Badmin2%5D%5Bwho%5D=admin2&sort%5Badmin%5D%5Badmin2%5D%5B2%5D=test', 'mode' => QueryComposeMode::Native, ]; yield 'basic encoding from php-src tests 8 - array with rfc1738 encoding' => [ 'variable' => [ 'name' => 'main page', 'sort' => 'desc,admin', 'equation' => '10 + 10 - 5', ], 'separator' => '&', 'encoding' => PHP_QUERY_RFC1738, 'expected' => 'name=main+page&sort=desc%2Cadmin&equation=10+%2B+10+-+5', 'mode' => QueryComposeMode::Native, ]; yield 'basic encoding from php-src tests 8 - array with rfc3986 encoding' => [ 'variable' => [ 'name' => 'main page', 'sort' => 'desc,admin', 'equation' => '10 + 10 - 5', ], 'separator' => '&', 'encoding' => PHP_QUERY_RFC3986, 'expected' => 'name=main%20page&sort=desc%2Cadmin&equation=10%20%2B%2010%20-%205', 'mode' => QueryComposeMode::Native, ]; yield 'basic encoding from php-src tests 8 - with null in default mode' => [ 'variable' => [null], 'separator' => '&', 'encoding' => PHP_QUERY_RFC3986, 'expected' => '', 'mode' => QueryComposeMode::Native, ]; yield 'basic encoding from php-src tests 8 - with null in conservative mode' => [ 'variable' => [null], 'separator' => '&', 'encoding' => PHP_QUERY_RFC3986, 'expected' => '', 'mode' => QueryComposeMode::Safe, ]; $v = 'value'; $ref = &$v; yield 'basic encoding from php-src tests 8 - with reference' => [ 'variable' => [$ref], 'separator' => '&', 'encoding' => PHP_QUERY_RFC3986, 'expected' => '0=value', 'mode' => QueryComposeMode::Native, ]; yield 'bug resolution in php-src tests 9 - float conversion' => [ 'variable' => ['x' => 1E+14, 'y' => '1E+14'], 'separator' => '&', 'encoding' => PHP_QUERY_RFC3986, 'expected' => 'x=1.0E%2B14&y=1E%2B14', 'mode' => QueryComposeMode::Native, ]; yield 'basic encoding from php-src tests backed enum' => [ 'variable' => ['backed' => EnumBacked::Two], 'separator' => '&', 'encoding' => PHP_QUERY_RFC3986, 'expected' => 'backed%5Bname%5D=Two&backed%5Bvalue%5D=Kabiri', 'mode' => QueryComposeMode::Compatible, ]; yield 'basic encoding from php-src tests backed enum in modern handled form' => [ 'variable' => ['backed' => EnumBacked::Two], 'separator' => '&', 'encoding' => PHP_QUERY_RFC3986, 'expected' => 'backed=Kabiri', 'mode' => QueryComposeMode::EnumCompatible, ]; } public function test_it_throws_if_a_object_recursion_is_detected(): void { $recursive = new class () { public mixed $public = 'input'; }; $recursive->public = $recursive; self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::Native)); self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::Compatible)); self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::EnumCompatible)); $this->expectException(TypeError::class); QueryString::compose($recursive, composeMode: QueryComposeMode::Safe); } public function test_it_throws_if_a_array_recursion_is_detected(): void { $recursive = []; $recursive['self'] = &$recursive; self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::Native)); self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::Compatible)); self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::EnumCompatible)); $this->expectException(ValueError::class); self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::Safe)); } public function test_it_throws_if_a_resource_is_present(): void { $tmpfile = [tmpfile()]; self::assertSame('', QueryString::compose($tmpfile, composeMode: QueryComposeMode::Native)); self::assertSame('', QueryString::compose($tmpfile, composeMode: QueryComposeMode::Compatible)); self::assertSame('', QueryString::compose($tmpfile, composeMode: QueryComposeMode::EnumCompatible)); $this->expectException(TypeError::class); QueryString::compose($tmpfile, composeMode: QueryComposeMode::Safe); } public function test_it_throws_if_a_non_backed_enum_is_given_in_enum_native_mode(): void { $this->expectException(TypeError::class); QueryString::compose(['pure' => PureEnum::One], composeMode: QueryComposeMode::EnumCompatible); } public function test_it_silently_ignore_if_a_non_backed_enum_is_given_in_enum_lenient_mode(): void { self::assertSame('foo=bar', QueryString::compose(['pure' => PureEnum::One, 'foo' => 'bar'], composeMode: QueryComposeMode::EnumLenient)); } public function test_it_throws_if_a_non_backed_enum_is_given_in_strict_mode(): void { $this->expectException(TypeError::class); QueryString::compose(['pure' => PureEnum::One], composeMode: QueryComposeMode::Safe); } public function test_it_does_not_fail_if_a_non_backed_enum_is_given_in_compatible_mode(): void { self::assertSame('pure%5Bname%5D=One', QueryString::compose(['pure' => PureEnum::One], composeMode: QueryComposeMode::Compatible)); } public function test_it_handles_backed_enums(): void { $params = ['bar' => EnumBacked::One, 'baz' => 1]; $compatible = 'bar%5Bname%5D=One&bar%5Bvalue%5D=Rimwe'; $enumNative = 'bar=Rimwe'; self::assertSame((PHP_VERSION_ID < 80400 ? $compatible : $enumNative).'&baz=1', QueryString::compose($params, composeMode: QueryComposeMode::Native)); self::assertSame($compatible.'&baz=1', QueryString::compose($params, composeMode: QueryComposeMode::Compatible)); self::assertSame($enumNative.'&baz=1', QueryString::compose($params, composeMode: QueryComposeMode::EnumLenient)); self::assertSame($enumNative.'&baz=1', QueryString::compose($params, composeMode: QueryComposeMode::EnumCompatible)); self::assertSame($enumNative.'&baz=1', QueryString::compose($params, composeMode: QueryComposeMode::Safe)); } public function test_it_can_handles_empty_array(): void { self::assertSame('', QueryString::compose([], composeMode: QueryComposeMode::Native)); self::assertSame('', QueryString::compose([], composeMode: QueryComposeMode::Compatible)); self::assertSame('', QueryString::compose([], composeMode: QueryComposeMode::EnumLenient)); self::assertSame('', QueryString::compose([], composeMode: QueryComposeMode::EnumCompatible)); self::assertNull(QueryString::compose([], composeMode: QueryComposeMode::Safe)); } public function test_it_can_convert_list_without_indices_in_safe_mode(): void { $data = ['a' => ['foo', false, 1.23]]; self::assertSame('a%5B%5D=foo&a%5B%5D=0&a%5B%5D=1.23', QueryString::compose($data, composeMode: QueryComposeMode::Safe)); self::assertSame('a%5B0%5D=foo&a%5B1%5D=0&a%5B2%5D=1.23', QueryString::compose($data, composeMode: QueryComposeMode::Native)); } public function test_it_can_handle_null_value_differently_with_composed_mode(): void { $data = ['module' => null, 'action' => '', 'page' => true]; self::assertSame('module&action=&page=1', QueryString::compose($data, composeMode: QueryComposeMode::Safe)); self::assertSame('action=&page=1', QueryString::compose($data, composeMode: QueryComposeMode::EnumLenient)); self::assertSame('action=&page=1', QueryString::compose($data, composeMode: QueryComposeMode::EnumCompatible)); self::assertSame('action=&page=1', QueryString::compose($data, composeMode: QueryComposeMode::Compatible)); self::assertSame('action=&page=1', QueryString::compose($data, composeMode: QueryComposeMode::Native)); self::assertSame('action=&page=1', http_build_query($data)); } } enum PureEnum { case One; case Two; } enum EnumBacked: string { case One = 'Rimwe'; case Two = 'Kabiri'; }