Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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