1<?php 2namespace GuzzleHttp; 3 4/** 5 * Manages query string variables and can aggregate them into a string 6 */ 7class Query extends Collection 8{ 9 const RFC3986 = 'RFC3986'; 10 const RFC1738 = 'RFC1738'; 11 12 /** @var callable Encoding function */ 13 private $encoding = 'rawurlencode'; 14 /** @var callable */ 15 private $aggregator; 16 17 /** 18 * Parse a query string into a Query object 19 * 20 * $urlEncoding is used to control how the query string is parsed and how 21 * it is ultimately serialized. The value can be set to one of the 22 * following: 23 * 24 * - true: (default) Parse query strings using RFC 3986 while still 25 * converting "+" to " ". 26 * - false: Disables URL decoding of the input string and URL encoding when 27 * the query string is serialized. 28 * - 'RFC3986': Use RFC 3986 URL encoding/decoding 29 * - 'RFC1738': Use RFC 1738 URL encoding/decoding 30 * 31 * @param string $query Query string to parse 32 * @param bool|string $urlEncoding Controls how the input string is decoded 33 * and encoded. 34 * @return self 35 */ 36 public static function fromString($query, $urlEncoding = true) 37 { 38 static $qp; 39 if (!$qp) { 40 $qp = new QueryParser(); 41 } 42 43 $q = new static(); 44 45 if ($urlEncoding !== true) { 46 $q->setEncodingType($urlEncoding); 47 } 48 49 $qp->parseInto($q, $query, $urlEncoding); 50 51 return $q; 52 } 53 54 /** 55 * Convert the query string parameters to a query string string 56 * 57 * @return string 58 */ 59 public function __toString() 60 { 61 if (!$this->data) { 62 return ''; 63 } 64 65 // The default aggregator is statically cached 66 static $defaultAggregator; 67 68 if (!$this->aggregator) { 69 if (!$defaultAggregator) { 70 $defaultAggregator = self::phpAggregator(); 71 } 72 $this->aggregator = $defaultAggregator; 73 } 74 75 $result = ''; 76 $aggregator = $this->aggregator; 77 $encoder = $this->encoding; 78 79 foreach ($aggregator($this->data) as $key => $values) { 80 foreach ($values as $value) { 81 if ($result) { 82 $result .= '&'; 83 } 84 $result .= $encoder($key); 85 if ($value !== null) { 86 $result .= '=' . $encoder($value); 87 } 88 } 89 } 90 91 return $result; 92 } 93 94 /** 95 * Controls how multi-valued query string parameters are aggregated into a 96 * string. 97 * 98 * $query->setAggregator($query::duplicateAggregator()); 99 * 100 * @param callable $aggregator Callable used to convert a deeply nested 101 * array of query string variables into a flattened array of key value 102 * pairs. The callable accepts an array of query data and returns a 103 * flattened array of key value pairs where each value is an array of 104 * strings. 105 */ 106 public function setAggregator(callable $aggregator) 107 { 108 $this->aggregator = $aggregator; 109 } 110 111 /** 112 * Specify how values are URL encoded 113 * 114 * @param string|bool $type One of 'RFC1738', 'RFC3986', or false to disable encoding 115 * 116 * @throws \InvalidArgumentException 117 */ 118 public function setEncodingType($type) 119 { 120 switch ($type) { 121 case self::RFC3986: 122 $this->encoding = 'rawurlencode'; 123 break; 124 case self::RFC1738: 125 $this->encoding = 'urlencode'; 126 break; 127 case false: 128 $this->encoding = function ($v) { return $v; }; 129 break; 130 default: 131 throw new \InvalidArgumentException('Invalid URL encoding type'); 132 } 133 } 134 135 /** 136 * Query string aggregator that does not aggregate nested query string 137 * values and allows duplicates in the resulting array. 138 * 139 * Example: http://test.com?q=1&q=2 140 * 141 * @return callable 142 */ 143 public static function duplicateAggregator() 144 { 145 return function (array $data) { 146 return self::walkQuery($data, '', function ($key, $prefix) { 147 return is_int($key) ? $prefix : "{$prefix}[{$key}]"; 148 }); 149 }; 150 } 151 152 /** 153 * Aggregates nested query string variables using the same technique as 154 * ``http_build_query()``. 155 * 156 * @param bool $numericIndices Pass false to not include numeric indices 157 * when multi-values query string parameters are present. 158 * 159 * @return callable 160 */ 161 public static function phpAggregator($numericIndices = true) 162 { 163 return function (array $data) use ($numericIndices) { 164 return self::walkQuery( 165 $data, 166 '', 167 function ($key, $prefix) use ($numericIndices) { 168 return !$numericIndices && is_int($key) 169 ? "{$prefix}[]" 170 : "{$prefix}[{$key}]"; 171 } 172 ); 173 }; 174 } 175 176 /** 177 * Easily create query aggregation functions by providing a key prefix 178 * function to this query string array walker. 179 * 180 * @param array $query Query string to walk 181 * @param string $keyPrefix Key prefix (start with '') 182 * @param callable $prefixer Function used to create a key prefix 183 * 184 * @return array 185 */ 186 public static function walkQuery(array $query, $keyPrefix, callable $prefixer) 187 { 188 $result = []; 189 foreach ($query as $key => $value) { 190 if ($keyPrefix) { 191 $key = $prefixer($key, $keyPrefix); 192 } 193 if (is_array($value)) { 194 $result += self::walkQuery($value, $key, $prefixer); 195 } elseif (isset($result[$key])) { 196 $result[$key][] = $value; 197 } else { 198 $result[$key] = array($value); 199 } 200 } 201 202 return $result; 203 } 204} 205