FbpParser   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 337
Duplicated Lines 5.34 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 92.31%

Importance

Changes 0
Metric Value
wmc 47
lcom 1
cbo 3
dl 18
loc 337
ccs 156
cts 169
cp 0.9231
rs 8.439
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 1
B run() 0 35 5
D examineSubset() 18 85 20
A hasValue() 0 8 2
A addPort() 0 7 1
B examineDefinition() 0 23 5
A examineProcess() 0 16 3
A addName() 0 4 1
A doSkip() 0 19 4
A validate() 0 12 4
A validationError() 0 6 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 FbpParser 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 FbpParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * This file is part of the phpflo\phpflo-fbp package.
4
 *
5
 * (c) Marc Aschmann <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
declare(strict_types=1);
11
namespace PhpFlo\Fbp;
12
13
use PhpFlo\Common\DefinitionInterface;
14
use PhpFlo\Common\FbpDefinitionsInterface;
15
use PhpFlo\Exception\ParserDefinitionException;
16
use PhpFlo\Exception\ParserException;
17
18
/**
19
 * Class FbpParser
20
 *
21
 * @package PhpFlo\Parser
22
 * @author Marc Aschmann <[email protected]>
23
 */
24
final class FbpParser implements FbpDefinitionsInterface
25
{
26
27
    /**
28
     * @var string
29
     */
30
    private $source;
31
32
    /**
33
     * @var array
34
     */
35
    private $settings;
36
37
    /**
38
     * @var array
39
     */
40
    private $schema;
41
42
    /**
43
     * @var int
44
     */
45
    private $linecount;
46
47
    /**
48
     * @var int
49
     */
50
    private $linecountOverall;
51
52
    /**
53
     * @var array
54
     */
55
    private $definition;
56
57
    /**
58
     * FbpParser constructor.
59
     *
60
     * @param string $source optional for initializing
61 7
     * @param array $settings optional settings for parser
62
     */
63 7
    public function __construct(string $source = '', array $settings = [])
64 7
    {
65 7
        $this->source   = $source;
66
        $this->settings = array_replace_recursive(
67 7
            [],
68
            $settings
69 7
        );
70 7
71 7
        $this->schema = [
72 7
            self::PROPERTIES_LABEL => [
73 7
                'name' => '',
74 7
            ],
75 7
            self::INITIALIZERS_LABEL => [],
76
            self::PROCESSES_LABEL => [],
77
            self::CONNECTIONS_LABEL => [],
78 7
        ];
79 7
80
        $this->definition = [];
81
    }
82
83
    /**
84
     * @param string $source
85
     * @return DefinitionInterface
86 7
     * @throws ParserException
87
     */
88 7
    public function run(string $source = '') : DefinitionInterface
89 2
    {
90 2
        if ('' != $source) {
91
            $this->source = $source;
92 7
        }
93 1
94
        if (empty($this->source)) {
95
            throw new ParserException("FbpParser::run(): no source data or empty string given!");
96 6
        }
97 6
98 6
        $this->definition = $this->schema; // reset
99
        $this->linecount = 1;
100
        $this->linecountOverall = 0;
101
102
        /*
103
         * split by lines, OS-independent
104 6
         * work each line and parse for definitions
105 6
         */
106
        foreach (preg_split('/' . self::NEWLINES . '/m', $this->source) as $line) {
107 6
            $this->linecountOverall++;
108 1
            // skip lines if empty or comments
109
            if ($this->doSkip($line)) {
110 6
                continue;
111 6
            }
112 6
            $subset = $this->examineSubset($line);
113 6
            $this->validate($subset, $line); // post-parse validation, easier that way
114 6
            $this->definition[self::CONNECTIONS_LABEL] = array_merge_recursive(
115
                $this->definition[self::CONNECTIONS_LABEL], $subset
116 6
            );
117 6
118
            $this->linecount;
119 5
        }
120
121
        return new FbpDefinition($this->definition);
122
    }
123
124
    /**
125
     * @param string $line
126
     * @return array
127 6
     * @throws ParserDefinitionException
128
     */
129 6
    private function examineSubset(string $line) : array
130 6
    {
131 6
        $subset = [];
132 6
        $step = [];
133
        $nextSrc = null;
134 6
        $hasInitializer = false;
135 2
136 2
        if (1 == $this->linecount && 0 === strpos(trim($line), "'")) {
137
            $hasInitializer = true;
138
        }
139 6
140 6
        // subset
141
        foreach (explode(self::SOURCE_TARGET_SEPARATOR, $line) as $definition) {
142 6
            $resolved = [];
143 6
144 6
            if (!$hasInitializer) {
145
                $resolved = $this->examineDefinition($definition);
146 6
            }
147 6
148
            $hasInport = $this->hasValue($resolved, self::INPORT_LABEL);
149
            $hasOutport = $this->hasValue($resolved, self::OUTPORT_LABEL);
150 6
151 6
            //define states
152
            switch (true) {
153 1 View Code Duplication
                case !empty($step[self::DATA_LABEL]) && ($hasInport && $hasOutport):
154 1
                    // initializer + inport
155
                    $nextSrc = $resolved;
156 1
                    $step[self::TARGET_LABEL] = $this->addPort($resolved, self::INPORT_LABEL);
157 1
                    // multi def oneliner initializer resolved
158 1
                    array_push($this->definition[self::INITIALIZERS_LABEL], $step);
159 6
                    $step = [];
160
                    break;
161
                case !empty($nextSrc) && ($hasInport && $hasOutport):
162 1
                    // if there was an initializer, we get a full touple with this iteration
163 1
                    $step = [
164 1
                        self::SOURCE_LABEL => $this->addPort($nextSrc, self::OUTPORT_LABEL),
165 1
                        self::TARGET_LABEL => $this->addPort($resolved, self::INPORT_LABEL),
166 1
                    ];
167 1
                    $nextSrc = $resolved;
168 1
                    array_push($subset, $step);
169 6
                    $step = [];
170
                    break;
171 3 View Code Duplication
                case $hasInport && $hasOutport:
172 3
                    // tgt + multi def
173
                    $nextSrc = $resolved;
174 3
                    $step[self::TARGET_LABEL] = $this->addPort($resolved, self::INPORT_LABEL);
175 3
                    // check if we've already got the touple ready
176 3
                    if (!empty($step[self::SOURCE_LABEL])) {
177 3
                        array_push($subset, $step);
178 3
                        $step = [];
179 6
                    }
180
                    break;
181 3
                case $hasInport && $nextSrc:
182 3
                    // use previous OUT as src to fill touple
183 6
                    $step[self::SOURCE_LABEL] = $this->addPort($nextSrc, self::OUTPORT_LABEL);
184 6
                    $nextSrc = null;
185
                case $hasInport:
186 6
                    $step[self::TARGET_LABEL] = $this->addPort($resolved, self::INPORT_LABEL);
187 6
                    // resolved touple
188 6
                    if (empty($step[self::DATA_LABEL])) {
189 1
                        array_push($subset, $step);
190
                    } else {
191 6
                        array_push($this->definition[self::INITIALIZERS_LABEL], $step);
192 6
                    }
193 6
                    $nextSrc = null;
194 6
                    $step = [];
195
                    break;
196 6
                case $hasOutport:
197 6
                    // simplest case OUT -> IN
198 2
                    $step[self::SOURCE_LABEL] = $this->addPort($resolved, self::OUTPORT_LABEL);
199
                    break;
200 2
                case $hasInitializer:
201 2
                    // initialization value: at the moment we only support one
202 2
                    $step[self::DATA_LABEL] = trim($definition, " '");
203
                    $hasInitializer = false; // reset
204
                    break;
205
                default:
206
                    throw new ParserDefinitionException(
207
                        "Line ({$this->linecountOverall}) {$line} does not contain in or out ports!"
208 6
                    );
209
            }
210 6
        }
211
212
        return $subset;
213
    }
214
215
    /**
216
     * Check if array has a specific key and is not empty.
217
     *
218
     * @param array $check
219
     * @param string $value
220 6
     * @return bool
221
     */
222 6
    private function hasValue(array $check, string $value) : bool
223 6
    {
224
        if (empty($check[$value])) {
225
            return false;
226 6
        }
227
228
        return true;
229
    }
230
231
    /**
232
     * @param array $definition
233
     * @param string $label
234 6
     * @return array
235
     */
236
    private function addPort(array $definition, string $label) : array
237 6
    {
238 6
        return [
239 6
            self::PROCESS_LABEL => $definition[self::PROCESS_LABEL],
240
            self::PORT_LABEL => $definition[$label],
241
        ];
242
    }
243
244
    /**
245
     * @param string $line
246
     * @return array
247 6
     * @throws ParserDefinitionException
248
     */
249 6
    private function examineDefinition(string $line) : array
250 6
    {
251 6
        preg_match('/' . self::PROCESS_DEFINITION . '/', $line, $matches);
252 6
        foreach ($matches as $key => $value) {
253 6
            if (is_numeric($key)) {
254 6
                unset($matches[$key]);
255
            }
256 6
        }
257 6
258 5
        if (!empty($matches[self::PROCESS_LABEL])) {
259 5
            if (empty($matches[self::COMPONENT_LABEL])) {
260
                $matches[self::COMPONENT_LABEL] = $matches[self::PROCESS_LABEL];
261 6
            }
262 6
263
            $this->examineProcess($matches);
264
        } else {
265
            throw new ParserDefinitionException(
266
                "No process definition found in line ({$this->linecountOverall}) {$line}"
267
            );
268 6
        }
269
270
        return $matches;
271
    }
272
273
    /**
274
     * Add entry to processes.
275
     *
276 6
     * @param array $process
277
     */
278 6
    private function examineProcess(array $process)
279 6
    {
280 6
        if (!isset($this->definition[self::PROCESSES_LABEL][$process[self::PROCESS_LABEL]])) {
281
            $component = $process[self::COMPONENT_LABEL];
282
            if (empty($component)) {
283
                $component = $process[self::PROCESS_LABEL];
284 6
            }
285 6
286 6
            $this->definition[self::PROCESSES_LABEL][$process[self::PROCESS_LABEL]] = [
287 6
                self::COMPONENT_LABEL => $component,
288 6
                self::METADATA_LABEL => [
289
                    'label' => $component,
290 6
                ],
291 6
            ];
292
        }
293
    }
294
295
    /**
296
     * Add name to definition
297
     *
298 1
     * @param string $line
299
     */
300 1
    private function addName(string $line)
301 1
    {
302
        $this->definition[self::PROPERTIES_LABEL]['name'] = trim(str_replace('#', '', $line));
303
    }
304
305
    /**
306
     * Check if line is empty or has comment.
307
     * In case of comments, add name to definition.
308
     *
309
     * @param string $line
310 6
     * @return bool
311
     */
312 6
    private function doSkip(string $line) : bool
313 6
    {
314
        switch (true) {
315 1
            case (empty(trim($line))):
316 1
                // empty line
317 6
                $skip = true;
318 1
                break;
319 1
            case (1 == preg_match('/(#[\s\w]+)/', $line)):
320 1
                if (1 === $this->linecountOverall) {
321 1
                    $this->addName($line);
322 1
                }
323 6
                $skip = true;
324 6
                break;
325 6
            default:
326
                $skip = false;
327 6
        }
328
329
        return $skip;
330
    }
331
332
    /**
333
     * @param array $subset
334 6
     * @param string $line
335
     */
336 6
    private function validate(array $subset, string $line)
337 6
    {
338 1
        foreach ($subset as $touple) {
339
            if (empty($touple[self::SOURCE_LABEL])) {
340
                $this->validationError($line, self::SOURCE_LABEL);
341 6
            }
342
343
            if (empty($touple[self::TARGET_LABEL])) {
344 6
                $this->validationError($line, self::TARGET_LABEL);
345 6
            }
346
        }
347
    }
348
349
    /**
350
     * @param string $line
351
     * @param string $port
352 1
     * @throws ParserException
353
     */
354 1
    private function validationError(string $line, string $port)
355 1
    {
356 1
        throw new ParserException(
357
            "Error on line ({$this->linecountOverall}) {$line}: There is no {$port} defined. Maybe you forgot an in or out port?"
358
        );
359
    }
360
}
361