Segment   B
last analyzed

Complexity

Total Complexity 44

Size/Duplication

Total Lines 303
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 44
c 3
b 1
f 0
lcom 1
cbo 3
dl 0
loc 303
rs 8.3396

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B parse() 0 18 5
A compile() 0 8 1
A getRegex() 0 8 2
A getTokens() 0 8 2
C parsePattern() 0 56 10
C buildRegex() 0 39 8
C buildString() 0 67 15

How to fix   Complexity   

Complex Class

Complex classes like Segment 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 Segment, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Dash
4
 *
5
 * @link      http://github.com/DASPRiD/Dash For the canonical source repository
6
 * @copyright 2013-2015 Ben Scholzen 'DASPRiD'
7
 * @license   http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
8
 */
9
10
namespace Dash\Parser;
11
12
use Dash\Exception;
13
14
class Segment implements ParserInterface
15
{
16
    /**#@+
17
     * Descriptive part elements.
18
     */
19
    const TYPE       = 0;
20
    const NAME       = 1;
21
    const LITERAL    = 1;
22
    const DELIMITERS = 2;
23
    /**#@-*/
24
25
    /**
26
     * @var string
27
     */
28
    protected $delimiter;
29
30
    /**
31
     * @var string
32
     */
33
    protected $pattern;
34
35
    /**
36
     * @var array
37
     */
38
    protected $constraints;
39
40
    /**
41
     * @var array
42
     */
43
    protected $tokens;
44
45
    /**
46
     * @var string
47
     */
48
    protected $regex;
49
50
    /**
51
     * @var array
52
     */
53
    protected $paramMap = [];
54
55
    /**
56
     * @param string $delimiter
57
     * @param string $pattern
58
     * @param array  $constraints
59
     */
60
    public function __construct($delimiter, $pattern, array $constraints)
61
    {
62
        $this->delimiter   = $delimiter;
63
        $this->pattern     = $pattern;
64
        $this->constraints = $constraints;
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70
    public function parse($input, $offset)
71
    {
72
        $result = preg_match('(\G' . $this->getRegex() . ')', $input, $matches, null, $offset);
73
74
        if (!$result) {
75
            return null;
76
        }
77
78
        $params = [];
79
80
        foreach ($this->paramMap as $index => $name) {
81
            if (isset($matches[$index]) && $matches[$index] !== '') {
82
                $params[$name] = $matches[$index];
83
            }
84
        }
85
86
        return new ParseResult($params, strlen($matches[0]));
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92
    public function compile(array $params, array $defaults)
93
    {
94
        return $this->buildString(
95
            $this->getTokens(),
96
            array_merge($defaults, $params),
97
            $defaults
98
        );
99
    }
100
101
    /**
102
     * Gets regex for matching.
103
     *
104
     * @return string
105
     */
106
    protected function getRegex()
107
    {
108
        if ($this->regex === null) {
109
            $this->regex = $this->buildRegex($this->getTokens(), $this->constraints);
110
        }
111
112
        return $this->regex;
113
    }
114
115
    /**
116
     * Gets parsed tokens.
117
     *
118
     * @return array
119
     */
120
    protected function getTokens()
121
    {
122
        if ($this->tokens === null) {
123
            $this->tokens = $this->parsePattern($this->pattern);
124
        }
125
126
        return $this->tokens;
127
    }
128
129
    /**
130
     * Parses a pattern.
131
     *
132
     * @param  string $pattern
133
     * @return array
134
     * @throws Exception\RuntimeException
135
     */
136
    protected function parsePattern($pattern)
137
    {
138
        $currentPos      = 0;
139
        $length          = strlen($pattern);
140
        $tokens          = [];
141
        $level           = 0;
142
        $quotedDelimiter = preg_quote($this->delimiter);
143
144
        while ($currentPos < $length) {
145
            preg_match('(\G(?P<literal>[^:{\[\]]*)(?P<token>[:\[\]]|$))', $pattern, $matches, 0, $currentPos);
146
147
            $currentPos += strlen($matches[0]);
148
149
            if (!empty($matches['literal'])) {
150
                $tokens[] = ['literal', $matches['literal']];
151
            }
152
153
            if ($matches['token'] === ':') {
154
                if (!preg_match(
155
                    '(\G(?P<name>[^:' . $quotedDelimiter . '{\[\]]+)(?:{(?P<delimiters>[^}]+)})?:?)',
156
                    $pattern,
157
                    $matches,
158
                    0,
159
                    $currentPos
160
                )) {
161
                    throw new Exception\RuntimeException('Found empty parameter name');
162
                }
163
164
                $tokens[] = [
165
                    'parameter',
166
                    $matches['name'],
167
                    isset($matches['delimiters']) ? $matches['delimiters'] : null
168
                ];
169
170
                $currentPos += strlen($matches[0]);
171
            } elseif ($matches['token'] === '[') {
172
                $tokens[] = ['optional-start'];
173
                $level++;
174
            } elseif ($matches['token'] === ']') {
175
                $tokens[] = ['optional-end'];
176
                $level--;
177
178
                if ($level < 0) {
179
                    throw new Exception\RuntimeException('Found closing bracket without matching opening bracket');
180
                }
181
            } else {
182
                break;
183
            }
184
        }
185
186
        if ($level > 0) {
187
            throw new Exception\RuntimeException('Found unbalanced brackets');
188
        }
189
190
        return $tokens;
191
    }
192
193
    /**
194
     * Builds the matching regex from parsed tokens.
195
     *
196
     * @param  array $tokens
197
     * @param  array $constraints
198
     * @return string
199
     */
200
    protected function buildRegex(array $tokens, array $constraints)
201
    {
202
        $groupIndex      = 1;
203
        $regex           = '';
204
        $quotedDelimiter = preg_quote($this->delimiter);
205
206
        foreach ($tokens as $token) {
207
            switch ($token[static::TYPE]) {
208
                case 'literal':
209
                    $regex .= preg_quote($token[static::LITERAL]);
210
                    break;
211
212
                case 'parameter':
213
                    $groupName = '?P<param' . $groupIndex . '>';
214
215
                    if (isset($constraints[$token[static::NAME]])) {
216
                        $regex .= '(' . $groupName . $constraints[$token[static::NAME]] . ')';
217
                    } elseif ($token[static::DELIMITERS] === null) {
218
                        $regex .= '(' . $groupName . '[^' . $quotedDelimiter . ']+)';
219
                    } else {
220
                        $regex .= '(' . $groupName . '[^' . $token[static::DELIMITERS] . ']+)';
221
                    }
222
223
                    $this->paramMap['param' . $groupIndex] = $token[static::NAME];
224
                    ++$groupIndex;
225
                    break;
226
227
                case 'optional-start':
228
                    $regex .= '(?:';
229
                    break;
230
231
                case 'optional-end':
232
                    $regex .= ')?';
233
                    break;
234
            }
235
        }
236
237
        return $regex;
238
    }
239
240
    /**
241
     * Builds a string from parts.
242
     *
243
     * @param  array $parts
244
     * @param  array $mergedParams
245
     * @param  array $defaults
246
     * @return string
247
     * @throws Exception\InvalidArgumentException
248
     */
249
    protected function buildString(array $parts, array $mergedParams, array $defaults)
250
    {
251
        $stack   = [];
252
        $current = [
253
            'is_optional' => false,
254
            'skip'        => true,
255
            'skippable'   => false,
256
            'path'        => '',
257
        ];
258
259
        foreach ($parts as $part) {
260
            switch ($part[static::TYPE]) {
261
                case 'literal':
262
                    $current['path'] .= $part[static::LITERAL];
263
                    break;
264
265
                case 'parameter':
266
                    $current['skippable'] = true;
267
268
                    if (!isset($mergedParams[$part[static::NAME]])) {
269
                        if (!$current['is_optional']) {
270
                            throw new Exception\InvalidArgumentException(sprintf(
271
                                'Missing parameter "%s"',
272
                                $part[static::NAME]
273
                            ));
274
                        }
275
276
                        continue;
277
                    } elseif (!$current['is_optional']
278
                        || !isset($defaults[$part[static::NAME]])
279
                        || $defaults[$part[static::NAME]] !== $mergedParams[$part[static::NAME]]
280
                    ) {
281
                        $current['skip'] = false;
282
                    }
283
284
                    $current['path'] .= $mergedParams[$part[static::NAME]];
285
                    break;
286
287
                case 'optional-start':
288
                    $stack[] = $current;
289
                    $current = [
290
                        'is_optional' => true,
291
                        'skip'        => true,
292
                        'skippable'   => false,
293
                        'path'        => '',
294
                    ];
295
                    break;
296
297
                case 'optional-end':
298
                    $parent = array_pop($stack);
299
300
                    if (!($current['path'] !== ''
301
                        && $current['is_optional']
302
                        && $current['skippable']
303
                        && $current['skip'])
304
                    ) {
305
                        $parent['path'] .= $current['path'];
306
                        $parent['skip'] = false;
307
                    }
308
309
                    $current = $parent;
310
                    break;
311
            }
312
        }
313
314
        return $current['path'];
315
    }
316
}
317