Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
peripherad
/
back
/
vendor
/
league
/
uri-interfaces
:
QueryString.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?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 BackedEnum; use League\Uri\Exceptions\SyntaxError; use League\Uri\KeyValuePair\Converter; use ReflectionEnum; use ReflectionException; use SplObjectStorage; use Stringable; use TypeError; use UnitEnum; use ValueError; use function array_is_list; use function array_key_exists; use function array_keys; use function get_debug_type; use function get_object_vars; use function http_build_query; use function implode; use function is_array; use function is_object; use function is_resource; use function is_scalar; use function rawurldecode; use function str_replace; use function strpos; use function substr; use const PHP_QUERY_RFC1738; 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; private const RECURSION_MARKER = "\0__RECURSION_INTERNAL_MARKER__\0"; /** * @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:mixed}> $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, StringCoercionMode $coercionMode = StringCoercionMode::Native): ?string { return self::buildFromPairs($pairs, Converter::fromEncodingType($encType)->withSeparator($separator), $coercionMode); } /** * 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:mixed}> $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, StringCoercionMode $coercionMode = StringCoercionMode::Native): ?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.'); } [$key, $value] = $pair; $coercionMode->isCoercible($value) || throw new SyntaxError('Converting a type `'.get_debug_type($value).'` into a string is not supported by the '.(StringCoercionMode::Native === $coercionMode ? 'PHP Native' : 'Ecmascript').' coercion mode.'); try { $key = $coercionMode->coerce($key); $value = $coercionMode->coerce($value); } catch (TypeError $typeError) { throw new SyntaxError('The pair can not be converted to build a query string.', previous: $typeError); } $keyValuePairs[] = [(string) Encoder::encodeQueryKeyValue($key), null === $value ? null : Encoder::encodeQueryKeyValue($value)]; } return ($converter ?? Converter::fromRFC3986())->toValue($keyValuePairs); } /** * Build a query string from an object or an array like http_build_query without discarding values. * The method differs from http_build_query for the following behavior: * * - if a resource is used, a TypeError is thrown. * - if a recursion is detected a ValueError is thrown * - the method preserves value with `null` value (http_build_query) skip the key. * - the method does not handle prefix usage * * @param array<array-key, mixed> $data * @param non-empty-string $separator * * @throws TypeError if a resource is found it the input array * @throws ValueError if a recursion is detected */ public static function compose( array|object $data, string $separator = '&', int $encType = PHP_QUERY_RFC1738, QueryComposeMode $composeMode = QueryComposeMode::Native ): ?string { if (QueryComposeMode::Native === $composeMode) { return http_build_query(data: $data, arg_separator: $separator, encoding_type: $encType); } $query = self::composeFromValue($data, Converter::fromEncodingType($encType)->withSeparator($separator), $composeMode); return QueryComposeMode::Safe !== $composeMode ? (string) $query : $query; } public static function composeFromValue( array|object $data, ?Converter $converter = null, QueryComposeMode $composeMode = QueryComposeMode::Native, ): ?string { if (QueryComposeMode::EnumLenient === $composeMode && $data instanceof UnitEnum && !$data instanceof BackedEnum) { return ''; } QueryComposeMode::Safe !== $composeMode || is_array($data) || throw new TypeError('In safe mode only arrays are supported.'); $converter ??= Converter::fromRFC3986(); $pairs = QueryComposeMode::Native !== $composeMode ? self::composeRecursive($composeMode, $data) : self::parseFromValue(http_build_query(data: $data, arg_separator: '&'), Converter::fromRFC1738()); return self::buildFromPairs($pairs, $converter); } /** * @param array<array-key, mixed>|object $data * @param SplObjectStorage<object, null> $seenObjects * * @throws TypeError if a resource is found it the input array * @throws ValueError if a recursion is detected * @throws ReflectionException if reflection is not possible on the Enum * * @return iterable<array{0: array-key, 1: string|int|float|bool|null}> */ private static function composeRecursive( QueryComposeMode $composeMode, array|object $data, string|int $prefix = '', SplObjectStorage $seenObjects = new SplObjectStorage(), ): iterable { QueryComposeMode::Safe !== $composeMode || is_array($data) || throw new TypeError('In safe mode only arrays are supported.'); in_array($composeMode, [QueryComposeMode::EnumCompatible, QueryComposeMode::EnumLenient], true) || !$data instanceof UnitEnum || throw new TypeError('Argument #1 ($data) must not be an enum, '.((new ReflectionEnum($data::class))->isBacked() ? 'Backed' : 'Pure').' given') ; if (is_object($data)) { if ($seenObjects->contains($data)) { QueryComposeMode::Safe !== $composeMode || throw new ValueError('composition failed; circular reference detected.'); return; } $seenObjects->attach($data); $data = get_object_vars($data); } if (self::hasCircularReference($data)) { QueryComposeMode::Safe !== $composeMode || throw new ValueError('composition failed; circular reference detected.'); return; } $stripIndices = QueryComposeMode::Safe === $composeMode && array_is_list($data); foreach ($data as $name => $value) { $name = $stripIndices ? '' : $name; if ('' !== $prefix) { $name = $prefix.'['.$name.']'; } if (is_resource($value)) { QueryComposeMode::Safe !== $composeMode || throw new TypeError('composition failed; a resource has been detected and can not be converted.'); continue; } if (is_scalar($value)) { yield [$name, $value]; continue; } if (null === $value) { if (QueryComposeMode::Safe === $composeMode) { yield [$name, $value]; } continue; } if ($value instanceof BackedEnum) { if (QueryComposeMode::Compatible !== $composeMode) { yield [$name, $value->value]; continue; } $value = get_object_vars($value); } if ($value instanceof UnitEnum) { if (QueryComposeMode::EnumLenient === $composeMode) { continue; } QueryComposeMode::Compatible === $composeMode || throw new TypeError('Unbacked enum '.$value::class.' cannot be converted to a string'); $value = get_object_vars($value); } if (QueryComposeMode::Safe === $composeMode && is_object($value)) { throw new ValueError('In conservative mode only arrays, scalar value or null are supported.'); } yield from self::composeRecursive($composeMode, $value, $name, $seenObjects); } } /** * Array recursion detection. * @see https://stackoverflow.com/questions/9042142/detecting-infinite-array-recursion-in-php */ private static function hasCircularReference(array &$arr): bool { if (isset($arr[self::RECURSION_MARKER])) { return true; } try { $arr[self::RECURSION_MARKER] = true; foreach ($arr as $key => &$value) { if (self::RECURSION_MARKER !== $key && is_array($value) && self::hasCircularReference($value)) { return true; } } return false; } finally { unset($arr[self::RECURSION_MARKER]); } } /** * Parses the query string. * * The result depends on the query parsing mode * * @see QueryString::extractFromValue() * * @param non-empty-string $separator * * @throws SyntaxError */ public static function extract( BackedEnum|Stringable|string|bool|null $query, string $separator = '&', int $encType = PHP_QUERY_RFC3986, QueryExtractMode $extractMode = QueryExtractMode::Unmangled, ): array { return self::extractFromValue( $query, Converter::fromEncodingType($encType)->withSeparator($separator), $extractMode, ); } /** * Parses the query string. * * The result depends on the query parsing mode * * @throws SyntaxError */ public static function extractFromValue( BackedEnum|Stringable|string|bool|null $query, ?Converter $converter = null, QueryExtractMode $extractMode = QueryExtractMode::Unmangled, ): array { $pairs = ($converter ?? Converter::fromRFC3986())->toPairs($query); if (QueryExtractMode::Native === $extractMode) { if ([] === $pairs) { return []; } $data = []; foreach ($pairs as [$key, $value]) { $key = str_replace('&', '%26', (string) $key); $data[] = null === $value ? $key : $key.'='.str_replace('&', '%26', $value); } parse_str(implode('&', $data), $result); return $result; } return self::convert( self::decodePairs($pairs, self::PAIR_VALUE_PRESERVED), $extractMode ); } /** * 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(BackedEnum|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(BackedEnum|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, QueryExtractMode $extractMode = QueryExtractMode::Unmangled): array { $returnedValue = []; foreach ($pairs as $pair) { $returnedValue = self::extractPhpVariable($returnedValue, $pair, extractMode: $extractMode); } 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 = '', QueryExtractMode $extractMode = QueryExtractMode::Unmangled ): array { if (is_array($name)) { [$name, $value] = $name; if (null !== $value || QueryExtractMode::LossLess !== $extractMode) { $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 ('' === $key) { $key = '0'; } if (!array_key_exists($key, $data) || !is_array($data[$key])) { $data[$key] = []; } $remaining = substr($name, $rightBracketPosition + 1); if (!str_starts_with($remaining, '[') || !str_contains($remaining, ']')) { $remaining = ''; } $name = substr($name, $leftBracketPosition + 1, $rightBracketPosition - $leftBracketPosition - 1).$remaining; if ('' === $name) { $data[$key][] = $value; return $data; } $data[$key] = self::extractPhpVariable($data[$key], $name, $value, $extractMode); return $data; } }