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