UriTemplate   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 245
Duplicated Lines 2.45 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 45
lcom 1
cbo 0
dl 6
loc 245
rs 8.3673
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A expand() 0 11 3
A setRegex() 0 4 1
B parseExpression() 0 34 5
F expandMatch() 6 120 34
A isAssoc() 0 4 1
A decodeReserved() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like UriTemplate often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UriTemplate, and based on these observations, apply Extract Interface, too.

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