3838use function rawurlencode ;
3939use function sprintf ;
4040use function str_replace ;
41+ use function str_split ;
4142use function strpos ;
42- use function strtoupper ;
4343use function substr ;
4444use const PHP_QUERY_RFC1738 ;
4545use const PHP_QUERY_RFC3986 ;
@@ -70,6 +70,9 @@ final class QueryString
7070 ],
7171 ];
7272
73+ private const DECODE_PAIR_VALUE = 1 ;
74+ private const PRESERVE_PAIR_VALUE = 2 ;
75+
7376 /**
7477 * @var string
7578 */
@@ -91,49 +94,86 @@ private function __construct()
9194 * Parses a query string into a collection of key/value pairs.
9295 *
9396 * @param null|mixed $query
97+ */
98+ public static function parse ($ query , string $ separator = '& ' , int $ enc_type = PHP_QUERY_RFC3986 ): array
99+ {
100+ $ query = self ::prepareQuery ($ query , $ enc_type );
101+ if (null === $ query ) {
102+ return [];
103+ }
104+
105+ if ('' === $ query ) {
106+ return [['' , null ]];
107+ }
108+
109+ $ retval = [];
110+ foreach (self ::getPairs ($ query , $ separator ) as $ pair ) {
111+ $ retval [] = self ::parsePair ((string ) $ pair , self ::DECODE_PAIR_VALUE );
112+ }
113+
114+ return $ retval ;
115+ }
116+
117+ /**
118+ * Prepare and normalize query before processing.
119+ *
120+ * @param null|mixed $query
94121 *
95- * @throws TypeError If the query is not stringable or the null value
96122 * @throws MalformedUriComponent If the query string is invalid
123+ * @throws TypeError If the query is not stringable or the null value
97124 * @throws UnknownEncoding If the encoding type is invalid
98125 */
99- public static function parse ($ query , string $ separator = ' & ' , int $ enc_type = PHP_QUERY_RFC3986 ): array
126+ private static function prepareQuery ($ query , int $ enc_type ): ? string
100127 {
101128 if (!isset (self ::ENCODING_LIST [$ enc_type ])) {
102129 throw new UnknownEncoding (sprintf ('Unknown Encoding: %s ' , $ enc_type ));
103130 }
104131
105132 if (null === $ query ) {
106- return [] ;
133+ return $ query ;
107134 }
108135
109- if (! is_scalar ($ query) && ! method_exists ( $ query , ' __toString ' )) {
110- throw new TypeError ( sprintf ( ' The query must be a scalar, a stringable object or the `null` value, `%s` given ' , gettype ( $ query ))) ;
136+ if (is_bool ($ query )) {
137+ return true === $ query ? ' 1 ' : ' 0 ' ;
111138 }
112139
113- if (is_bool ($ query )) {
114- return [[ $ query ? ' 1 ' : ' 0 ' , null ]] ;
140+ if (! is_scalar ($ query) && ! method_exists ( $ query , ' __toString ' )) {
141+ throw new TypeError ( sprintf ( ' The query must be a scalar, a stringable object or the ` null` value, `%s` given ' , gettype ( $ query ))) ;
115142 }
116143
117144 $ query = (string ) $ query ;
118145 if ('' === $ query ) {
119- return [[ '' , null ]] ;
146+ return $ query ;
120147 }
121148
122149 if (1 === preg_match (self ::REGEXP_INVALID_CHARS , $ query )) {
123150 throw new MalformedUriComponent (sprintf ('Invalid query string: %s ' , $ query ));
124151 }
125152
126153 if (PHP_QUERY_RFC1738 === $ enc_type ) {
127- $ query = str_replace ('+ ' , ' ' , $ query );
154+ return str_replace ('+ ' , ' ' , $ query );
155+ }
156+
157+ return $ query ;
158+ }
159+
160+ private static function getPairs (string $ query , string $ separator ): array
161+ {
162+ if ('' === $ separator ) {
163+ return str_split ($ query );
164+ }
165+
166+ if (false === strpos ($ query , $ separator )) {
167+ return [$ query ];
128168 }
129169
130- return array_map ([ self ::class, ' parsePair ' ], ( array ) explode ($ separator , $ query) );
170+ return ( array ) explode ($ separator , $ query );
131171 }
132172
133173 /**
134174 * Returns the key/value pair from a query string pair.
135175 */
136- private static function parsePair (string $ pair ): array
176+ private static function parsePair (string $ pair, int $ parseValue ): array
137177 {
138178 [$ key , $ value ] = explode ('= ' , $ pair , 2 ) + [1 => null ];
139179 $ key = (string ) $ key ;
@@ -146,7 +186,7 @@ private static function parsePair(string $pair): array
146186 return [$ key , $ value ];
147187 }
148188
149- if (1 === preg_match (self ::REGEXP_ENCODED_PATTERN , $ value )) {
189+ if ($ parseValue === self :: DECODE_PAIR_VALUE && 1 === preg_match (self ::REGEXP_ENCODED_PATTERN , $ value )) {
150190 $ value = preg_replace_callback (self ::REGEXP_ENCODED_PATTERN , [self ::class, 'decodeMatch ' ], $ value );
151191 }
152192
@@ -158,10 +198,6 @@ private static function parsePair(string $pair): array
158198 */
159199 private static function decodeMatch (array $ matches ): string
160200 {
161- if (1 === preg_match (self ::REGEXP_DECODED_PATTERN , $ matches [0 ])) {
162- return strtoupper ($ matches [0 ]);
163- }
164-
165201 return rawurldecode ($ matches [0 ]);
166202 }
167203
@@ -289,7 +325,17 @@ private static function encodeMatches(array $matches): string
289325 */
290326 public static function extract ($ query , string $ separator = '& ' , int $ enc_type = PHP_QUERY_RFC3986 ): array
291327 {
292- return self ::convert (self ::parse ($ query , $ separator , $ enc_type ));
328+ $ query = self ::prepareQuery ($ query , $ enc_type );
329+ if (null === $ query || '' === $ query ) {
330+ return [];
331+ }
332+
333+ $ retval = [];
334+ foreach (self ::getPairs ($ query , $ separator ) as $ pair ) {
335+ $ retval [] = self ::parsePair ((string ) $ pair , self ::PRESERVE_PAIR_VALUE );
336+ }
337+
338+ return self ::convert ($ retval );
293339 }
294340
295341 /**
0 commit comments