Completed
Pull Request — master (#1867)
by Bloody
02:15
created

UriTemplate::expandMatch()   F

Complexity

Conditions 27
Paths 58

Size

Total Lines 108

Duplication

Lines 8
Ratio 7.41 %

Importance

Changes 0
Metric Value
cc 27
nc 58
nop 1
dl 8
loc 108
rs 3.3333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
            if (!isset($this->variables[$value['value']])) {
111
                continue;
112
            }
113
114
            $variable = $this->variables[$value['value']];
115
            $actuallyUseQuery = $useQuery;
116
            $expanded = '';
117
118
            if (is_array($variable)) {
119
                $isAssoc = $this->isAssoc($variable);
120
                $kvp = [];
121
                foreach ($variable as $key => $var) {
122
                    if ($isAssoc) {
123
                        $key = rawurlencode($key);
124
                        $isNestedArray = is_array($var);
125
                    } else {
126
                        $isNestedArray = false;
127
                    }
128
129
                    if (!$isNestedArray) {
130
                        $var = rawurlencode($var);
131 View Code Duplication
                        if ($parsed['operator'] === '+' ||
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
132
                            $parsed['operator'] === '#'
133
                        ) {
134
                            $var = $this->decodeReserved($var);
135
                        }
136
                    }
137
138
                    if ($value['modifier'] === '*') {
139
                        if ($isAssoc) {
140
                            if ($isNestedArray) {
141
                                // Nested arrays must allow for deeply nested
142
                                // structures.
143
                                $var = strtr(
144
                                    http_build_query([$key => $var]),
145
                                    $rfc1738to3986
146
                                );
147
                            } else {
148
                                $var = $key . '=' . $var;
149
                            }
150
                        } elseif ($key > 0 && $actuallyUseQuery) {
151
                            $var = $value['value'] . '=' . $var;
152
                        }
153
                    }
154
155
                    $kvp[$key] = $var;
156
                }
157
158
                if (empty($variable)) {
159
                    $actuallyUseQuery = false;
160
                } elseif ($value['modifier'] === '*') {
161
                    $expanded = implode($joiner, $kvp);
162
                    if ($isAssoc) {
163
                        // Don't prepend the value name when using the explode
164
                        // modifier with an associative array.
165
                        $actuallyUseQuery = false;
166
                    }
167
                } else {
168
                    if ($isAssoc) {
169
                        // When an associative array is encountered and the
170
                        // explode modifier is not set, then the result must be
171
                        // a comma separated list of keys followed by their
172
                        // respective values.
173
                        foreach ($kvp as $k => &$v) {
174
                            $v = $k . ',' . $v;
175
                        }
176
                    }
177
                    $expanded = implode(',', $kvp);
178
                }
179
            } else {
180
                if ($value['modifier'] === ':') {
181
                    $variable = substr($variable, 0, $value['position']);
182
                }
183
                $expanded = rawurlencode($variable);
184 View Code Duplication
                if ($parsed['operator'] === '+' || $parsed['operator'] === '#') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
185
                    $expanded = $this->decodeReserved($expanded);
186
                }
187
            }
188
189
            if ($actuallyUseQuery) {
190
                if (!$expanded && $joiner !== '&') {
191
                    $expanded = $value['value'];
192
                } else {
193
                    $expanded = $value['value'] . '=' . $expanded;
194
                }
195
            }
196
197
            $replacements[] = $expanded;
198
        }
199
200
        $ret = implode($joiner, $replacements);
201
        if ($ret && $prefix) {
202
            return $prefix . $ret;
203
        }
204
205
        return $ret;
206
    }
207
208
    /**
209
     * Determines if an array is associative.
210
     *
211
     * This makes the assumption that input arrays are sequences or hashes.
212
     * This assumption is a tradeoff for accuracy in favor of speed, but it
213
     * should work in almost every case where input is supplied for a URI
214
     * template.
215
     *
216
     * @param array $array Array to check
217
     *
218
     * @return bool
219
     */
220
    private function isAssoc(array $array)
221
    {
222
        return $array && array_keys($array)[0] !== 0;
0 ignored issues
show
Bug Best Practice introduced by
The expression $array of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
223
    }
224
225
    /**
226
     * Removes percent encoding on reserved characters (used with + and #
227
     * modifiers).
228
     *
229
     * @param string $string String to fix
230
     *
231
     * @return string
232
     */
233
    private function decodeReserved($string)
234
    {
235
        return str_replace(self::$delimsPct, self::$delims, $string);
236
    }
237
}
238