123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- <?php
- /**
- * League.Uri (https://uri.thephpleague.com)
- *
- * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- declare(strict_types=1);
- namespace League\Uri;
- use League\Uri\Exceptions\SyntaxError;
- use League\Uri\KeyValuePair\Converter;
- use Stringable;
- use function array_key_exists;
- use function array_keys;
- use function is_array;
- use function rawurldecode;
- use function strpos;
- use function substr;
- use const PHP_QUERY_RFC3986;
- /**
- * A class to parse the URI query string.
- *
- * @see https://tools.ietf.org/html/rfc3986#section-3.4
- */
- final class QueryString
- {
- private const PAIR_VALUE_DECODED = 1;
- private const PAIR_VALUE_PRESERVED = 2;
- /**
- * @codeCoverageIgnore
- */
- private function __construct()
- {
- }
- /**
- * Build a query string from a list of pairs.
- *
- * @see QueryString::buildFromPairs()
- * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
- *
- * @param iterable<array{0:string, 1:string|float|int|bool|null}> $pairs
- * @param non-empty-string $separator
- *
- * @throws SyntaxError If the encoding type is invalid
- * @throws SyntaxError If a pair is invalid
- */
- public static function build(iterable $pairs, string $separator = '&', int $encType = PHP_QUERY_RFC3986): ?string
- {
- return self::buildFromPairs($pairs, Converter::fromEncodingType($encType)->withSeparator($separator));
- }
- /**
- * Build a query string from a list of pairs.
- *
- * The method expects the return value from Query::parse to build
- * a valid query string. This method differs from PHP http_build_query as
- * it does not modify parameters keys.
- *
- * If a reserved character is found in a URI component and
- * no delimiting role is known for that character, then it must be
- * interpreted as representing the data octet corresponding to that
- * character's encoding in US-ASCII.
- *
- * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
- *
- * @param iterable<array{0:string, 1:string|float|int|bool|null}> $pairs
- *
- * @throws SyntaxError If the encoding type is invalid
- * @throws SyntaxError If a pair is invalid
- */
- public static function buildFromPairs(iterable $pairs, ?Converter $converter = null): ?string
- {
- $keyValuePairs = [];
- foreach ($pairs as $pair) {
- if (!is_array($pair) || [0, 1] !== array_keys($pair)) {
- throw new SyntaxError('A pair must be a sequential array starting at `0` and containing two elements.');
- }
- $keyValuePairs[] = [(string) Encoder::encodeQueryKeyValue($pair[0]), match(null) {
- $pair[1] => null,
- default => Encoder::encodeQueryKeyValue($pair[1]),
- }];
- }
- return ($converter ?? Converter::fromRFC3986())->toValue($keyValuePairs);
- }
- /**
- * Parses the query string like parse_str without mangling the results.
- *
- * @see QueryString::extractFromValue()
- * @see http://php.net/parse_str
- * @see https://wiki.php.net/rfc/on_demand_name_mangling
- *
- * @param non-empty-string $separator
- *
- * @throws SyntaxError
- */
- public static function extract(Stringable|string|bool|null $query, string $separator = '&', int $encType = PHP_QUERY_RFC3986): array
- {
- return self::extractFromValue($query, Converter::fromEncodingType($encType)->withSeparator($separator));
- }
- /**
- * Parses the query string like parse_str without mangling the results.
- *
- * The result is similar as PHP parse_str when used with its
- * second argument with the difference that variable names are
- * not mangled.
- *
- * @see http://php.net/parse_str
- * @see https://wiki.php.net/rfc/on_demand_name_mangling
- *
- * @throws SyntaxError
- */
- public static function extractFromValue(Stringable|string|bool|null $query, ?Converter $converter = null): array
- {
- return self::convert(self::decodePairs(
- ($converter ?? Converter::fromRFC3986())->toPairs($query),
- self::PAIR_VALUE_PRESERVED
- ));
- }
- /**
- * Parses a query string into a collection of key/value pairs.
- *
- * @param non-empty-string $separator
- *
- * @throws SyntaxError
- *
- * @return array<int, array{0:string, 1:string|null}>
- */
- public static function parse(Stringable|string|bool|null $query, string $separator = '&', int $encType = PHP_QUERY_RFC3986): array
- {
- return self::parseFromValue($query, Converter::fromEncodingType($encType)->withSeparator($separator));
- }
- /**
- * Parses a query string into a collection of key/value pairs.
- *
- * @throws SyntaxError
- *
- * @return array<int, array{0:string, 1:string|null}>
- */
- public static function parseFromValue(Stringable|string|bool|null $query, ?Converter $converter = null): array
- {
- return self::decodePairs(
- ($converter ?? Converter::fromRFC3986())->toPairs($query),
- self::PAIR_VALUE_DECODED
- );
- }
- /**
- * @param array<non-empty-list<string|null>> $pairs
- *
- * @return array<int, array{0:string, 1:string|null}>
- */
- private static function decodePairs(array $pairs, int $pairValueState): array
- {
- $decodePair = static function (array $pair, int $pairValueState): array {
- [$key, $value] = $pair;
- return match ($pairValueState) {
- self::PAIR_VALUE_PRESERVED => [(string) Encoder::decodeAll($key), $value],
- default => [(string) Encoder::decodeAll($key), Encoder::decodeAll($value)],
- };
- };
- return array_reduce(
- $pairs,
- fn (array $carry, array $pair) => [...$carry, $decodePair($pair, $pairValueState)],
- []
- );
- }
- /**
- * Converts a collection of key/value pairs and returns
- * the store PHP variables as elements of an array.
- */
- public static function convert(iterable $pairs): array
- {
- $returnedValue = [];
- foreach ($pairs as $pair) {
- $returnedValue = self::extractPhpVariable($returnedValue, $pair);
- }
- return $returnedValue;
- }
- /**
- * Parses a query pair like parse_str without mangling the results array keys.
- *
- * <ul>
- * <li>empty name are not saved</li>
- * <li>If the value from name is duplicated its corresponding value will be overwritten</li>
- * <li>if no "[" is detected the value is added to the return array with the name as index</li>
- * <li>if no "]" is detected after detecting a "[" the value is added to the return array with the name as index</li>
- * <li>if there's a mismatch in bracket usage the remaining part is dropped</li>
- * <li>“.” and “ ” are not converted to “_”</li>
- * <li>If there is no “]”, then the first “[” is not converted to becomes an “_”</li>
- * <li>no whitespace trimming is done on the key value</li>
- * </ul>
- *
- * @see https://php.net/parse_str
- * @see https://wiki.php.net/rfc/on_demand_name_mangling
- * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic1.phpt
- * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic2.phpt
- * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic3.phpt
- * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic4.phpt
- *
- * @param array $data the submitted array
- * @param array|string $name the pair key
- * @param string $value the pair value
- */
- private static function extractPhpVariable(array $data, array|string $name, string $value = ''): array
- {
- if (is_array($name)) {
- [$name, $value] = $name;
- $value = rawurldecode((string) $value);
- }
- if ('' === $name) {
- return $data;
- }
- $leftBracketPosition = strpos($name, '[');
- if (false === $leftBracketPosition) {
- $data[$name] = $value;
- return $data;
- }
- $rightBracketPosition = strpos($name, ']', $leftBracketPosition);
- if (false === $rightBracketPosition) {
- $data[$name] = $value;
- return $data;
- }
- $key = substr($name, 0, $leftBracketPosition);
- if (!array_key_exists($key, $data) || !is_array($data[$key])) {
- $data[$key] = [];
- }
- $index = substr($name, $leftBracketPosition + 1, $rightBracketPosition - $leftBracketPosition - 1);
- if ('' === $index) {
- $data[$key][] = $value;
- return $data;
- }
- $remaining = substr($name, $rightBracketPosition + 1);
- if (!str_starts_with($remaining, '[') || false === strpos($remaining, ']', 1)) {
- $remaining = '';
- }
- $data[$key] = self::extractPhpVariable($data[$key], $index.$remaining, $value);
- return $data;
- }
- }
|