Subversion Repositories cheapmusic

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
103 - 1
<?php
2
namespace GuzzleHttp;
3
 
4
/**
5
 * Expands URI templates. Userland implementation of PECL uri_template.
6
 *
7
 * @link http://tools.ietf.org/html/rfc6570
8
 */
9
class UriTemplate
10
{
11
    /** @var string URI template */
12
    private $template;
13
 
14
    /** @var array Variables to use in the template expansion */
15
    private $variables;
16
 
17
    /** @var array Hash for quick operator lookups */
18
    private static $operatorHash = [
19
        ''  => ['prefix' => '',  'joiner' => ',', 'query' => false],
20
        '+' => ['prefix' => '',  'joiner' => ',', 'query' => false],
21
        '#' => ['prefix' => '#', 'joiner' => ',', 'query' => false],
22
        '.' => ['prefix' => '.', 'joiner' => '.', 'query' => false],
23
        '/' => ['prefix' => '/', 'joiner' => '/', 'query' => false],
24
        ';' => ['prefix' => ';', 'joiner' => ';', 'query' => true],
25
        '?' => ['prefix' => '?', 'joiner' => '&', 'query' => true],
26
        '&' => ['prefix' => '&', 'joiner' => '&', 'query' => true]
27
    ];
28
 
29
    /** @var array Delimiters */
30
    private static $delims = [':', '/', '?', '#', '[', ']', '@', '!', '$',
31
        '&', '\'', '(', ')', '*', '+', ',', ';', '='];
32
 
33
    /** @var array Percent encoded delimiters */
34
    private static $delimsPct = ['%3A', '%2F', '%3F', '%23', '%5B', '%5D',
35
        '%40', '%21', '%24', '%26', '%27', '%28', '%29', '%2A', '%2B', '%2C',
36
        '%3B', '%3D'];
37
 
38
    public function expand($template, array $variables)
39
    {
40
        if (false === strpos($template, '{')) {
41
            return $template;
42
        }
43
 
44
        $this->template = $template;
45
        $this->variables = $variables;
46
 
47
        return preg_replace_callback(
48
            '/\{([^\}]+)\}/',
49
            [$this, 'expandMatch'],
50
            $this->template
51
        );
52
    }
53
 
54
    /**
55
     * Parse an expression into parts
56
     *
57
     * @param string $expression Expression to parse
58
     *
59
     * @return array Returns an associative array of parts
60
     */
61
    private function parseExpression($expression)
62
    {
63
        $result = [];
64
 
65
        if (isset(self::$operatorHash[$expression[0]])) {
66
            $result['operator'] = $expression[0];
67
            $expression = substr($expression, 1);
68
        } else {
69
            $result['operator'] = '';
70
        }
71
 
72
        foreach (explode(',', $expression) as $value) {
73
            $value = trim($value);
74
            $varspec = [];
75
            if ($colonPos = strpos($value, ':')) {
76
                $varspec['value'] = substr($value, 0, $colonPos);
77
                $varspec['modifier'] = ':';
78
                $varspec['position'] = (int) substr($value, $colonPos + 1);
79
            } elseif (substr($value, -1) === '*') {
80
                $varspec['modifier'] = '*';
81
                $varspec['value'] = substr($value, 0, -1);
82
            } else {
83
                $varspec['value'] = (string) $value;
84
                $varspec['modifier'] = '';
85
            }
86
            $result['values'][] = $varspec;
87
        }
88
 
89
        return $result;
90
    }
91
 
92
    /**
93
     * Process an expansion
94
     *
95
     * @param array $matches Matches met in the preg_replace_callback
96
     *
97
     * @return string Returns the replacement string
98
     */
99
    private function expandMatch(array $matches)
100
    {
101
        static $rfc1738to3986 = ['+' => '%20', '%7e' => '~'];
102
 
103
        $replacements = [];
104
        $parsed = self::parseExpression($matches[1]);
105
        $prefix = self::$operatorHash[$parsed['operator']]['prefix'];
106
        $joiner = self::$operatorHash[$parsed['operator']]['joiner'];
107
        $useQuery = self::$operatorHash[$parsed['operator']]['query'];
108
 
109
        foreach ($parsed['values'] as $value) {
110
 
111
            if (!isset($this->variables[$value['value']])) {
112
                continue;
113
            }
114
 
115
            $variable = $this->variables[$value['value']];
116
            $actuallyUseQuery = $useQuery;
117
            $expanded = '';
118
 
119
            if (is_array($variable)) {
120
 
121
                $isAssoc = $this->isAssoc($variable);
122
                $kvp = [];
123
                foreach ($variable as $key => $var) {
124
 
125
                    if ($isAssoc) {
126
                        $key = rawurlencode($key);
127
                        $isNestedArray = is_array($var);
128
                    } else {
129
                        $isNestedArray = false;
130
                    }
131
 
132
                    if (!$isNestedArray) {
133
                        $var = rawurlencode($var);
134
                        if ($parsed['operator'] === '+' ||
135
                            $parsed['operator'] === '#'
136
                        ) {
137
                            $var = $this->decodeReserved($var);
138
                        }
139
                    }
140
 
141
                    if ($value['modifier'] === '*') {
142
                        if ($isAssoc) {
143
                            if ($isNestedArray) {
144
                                // Nested arrays must allow for deeply nested
145
                                // structures.
146
                                $var = strtr(
147
                                    http_build_query([$key => $var]),
148
                                    $rfc1738to3986
149
                                );
150
                            } else {
151
                                $var = $key . '=' . $var;
152
                            }
153
                        } elseif ($key > 0 && $actuallyUseQuery) {
154
                            $var = $value['value'] . '=' . $var;
155
                        }
156
                    }
157
 
158
                    $kvp[$key] = $var;
159
                }
160
 
161
                if (empty($variable)) {
162
                    $actuallyUseQuery = false;
163
                } elseif ($value['modifier'] === '*') {
164
                    $expanded = implode($joiner, $kvp);
165
                    if ($isAssoc) {
166
                        // Don't prepend the value name when using the explode
167
                        // modifier with an associative array.
168
                        $actuallyUseQuery = false;
169
                    }
170
                } else {
171
                    if ($isAssoc) {
172
                        // When an associative array is encountered and the
173
                        // explode modifier is not set, then the result must be
174
                        // a comma separated list of keys followed by their
175
                        // respective values.
176
                        foreach ($kvp as $k => &$v) {
177
                            $v = $k . ',' . $v;
178
                        }
179
                    }
180
                    $expanded = implode(',', $kvp);
181
                }
182
 
183
            } else {
184
                if ($value['modifier'] === ':') {
185
                    $variable = substr($variable, 0, $value['position']);
186
                }
187
                $expanded = rawurlencode($variable);
188
                if ($parsed['operator'] === '+' || $parsed['operator'] === '#') {
189
                    $expanded = $this->decodeReserved($expanded);
190
                }
191
            }
192
 
193
            if ($actuallyUseQuery) {
194
                if (!$expanded && $joiner !== '&') {
195
                    $expanded = $value['value'];
196
                } else {
197
                    $expanded = $value['value'] . '=' . $expanded;
198
                }
199
            }
200
 
201
            $replacements[] = $expanded;
202
        }
203
 
204
        $ret = implode($joiner, $replacements);
205
        if ($ret && $prefix) {
206
            return $prefix . $ret;
207
        }
208
 
209
        return $ret;
210
    }
211
 
212
    /**
213
     * Determines if an array is associative.
214
     *
215
     * This makes the assumption that input arrays are sequences or hashes.
216
     * This assumption is a tradeoff for accuracy in favor of speed, but it
217
     * should work in almost every case where input is supplied for a URI
218
     * template.
219
     *
220
     * @param array $array Array to check
221
     *
222
     * @return bool
223
     */
224
    private function isAssoc(array $array)
225
    {
226
        return $array && array_keys($array)[0] !== 0;
227
    }
228
 
229
    /**
230
     * Removes percent encoding on reserved characters (used with + and #
231
     * modifiers).
232
     *
233
     * @param string $string String to fix
234
     *
235
     * @return string
236
     */
237
    private function decodeReserved($string)
238
    {
239
        return str_replace(self::$delimsPct, self::$delims, $string);
240
    }
241
}