Completed
Push — master ( 8a4e5e...de568f )
by z38
02:27
created

Px   C

Complexity

Total Complexity 62

Size/Duplication

Total Lines 394
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 90.06%

Importance

Changes 8
Bugs 0 Features 3
Metric Value
wmc 62
c 8
b 0
f 3
lcom 1
cbo 0
dl 0
loc 394
ccs 163
cts 181
cp 0.9006
rs 5.9493

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A values() 0 9 3
A codes() 0 10 3
A index() 0 11 2
A datum() 0 11 2
A keywords() 0 6 1
A keywordList() 0 10 2
A hasKeyword() 0 6 1
A variables() 0 10 3
A keyword() 0 9 2
A data() 0 6 1
C parseKeywordLine() 0 40 11
B assertKeywords() 0 27 6
D assertData() 0 41 10
A assertIndexMultipliers() 0 15 3
A decodeLine() 0 10 3
A split() 0 11 2
A findQuoted() 0 12 3
A findQuotedReverse() 0 13 3

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace Z38\PcAxis;
4
5
use RuntimeException;
6
use stdClass;
7
8
/**
9
 * PC-Axis (PX) file reader
10
 */
11
class Px
12
{
13
    const DEFAULT_CHARSET = '437';
14
15
    /**
16
     * @var string
17
     */
18
    private $path;
19
20
    /**
21
     * @var resource
22
     */
23
    private $handle;
24
25
    /**
26
     * @var array
27
     */
28
    private $keywords;
29
30
    /**
31
     * @var array
32
     */
33
    private $data;
34
35
    /**
36
     * @var int
37
     */
38
    private $dataOffset;
39
40
    /**
41
     * @var string
42
     */
43
    private $charset;
44
45
    /**
46
     * @var string
47
     */
48
    private $codepage;
49
50
    /**
51
     * @var array
52
     */
53
    private $indexMultipliers;
54
55
    /**
56
     * Constructor
57
     *
58
     * @param string $path path to your PX file
59
     */
60 9
    public function __construct($path)
61
    {
62 9
        $this->path = $path;
63 9
        $this->charset = self::DEFAULT_CHARSET;
64 9
    }
65
66
    /**
67
     * Returns a list of all variables.
68
     *
69
     * @return array
70
     */
71 4
    public function variables()
72
    {
73 4
        if (!$this->hasKeyword('STUB')) {
74 1
            return $this->keyword('HEADING')->values;
75 3
        } elseif (!$this->hasKeyword('HEADING')) {
76
            return $this->keyword('STUB')->values;
77
        } else {
78 3
            return array_merge($this->keyword('STUB')->values, $this->keyword('HEADING')->values);
79
        }
80
    }
81
82
    /**
83
     * Returns a list of all possible values of a variable.
84
     *
85
     * @param string $variable
86
     *
87
     * @return array
88
     */
89 3
    public function values($variable)
90
    {
91 3
        foreach ($this->keywordList('VALUES') as $keyword) {
92 3
            if ($keyword->subKeys[0] == $variable) {
93 3
                return $keyword->values;
94
            }
95 3
        }
96
        throw new RuntimeException(sprintf('Could not determine values of "%s".', $variable));
97
    }
98
99
    /**
100
     * Returns a list of all possible codes of a variable.
101
     *
102
     * @param string $variable
103
     *
104
     * @return array|null
105
     */
106 2
    public function codes($variable)
107
    {
108 2
        foreach ($this->keywordList('CODES') as $keyword) {
109 2
            if ($keyword->subKeys[0] == $variable) {
110 1
                return $keyword->values;
111
            }
112 2
        }
113
114 1
        return null;
115
    }
116
117
    /**
118
     * Computes the index within the data matrix.
119
     *
120
     * @param array $indices An array of all value indices
121
     *
122
     * @return int
123
     */
124 2
    public function index($indices)
125
    {
126 2
        $this->assertIndexMultipliers();
127
128 2
        $index = 0;
129 2
        for ($i = 0, $length = count($this->indexMultipliers); $i < $length; ++$i) {
130 2
            $index += $indices[$i] * $this->indexMultipliers[$i];
131 2
        }
132
133 2
        return $index;
134
    }
135
136
    /**
137
     * Gets a single data point.
138
     *
139
     * @param array $indices An array of all value indices
140
     *
141
     * @return string
142
     */
143 2
    public function datum($indices)
144
    {
145 2
        $this->assertData();
146
147 2
        $index = $this->index($indices);
148 2
        if (isset($this->data[$index])) {
149 2
            return $this->data[$index];
150
        } else {
151
            return null;
152
        }
153
    }
154
155
    /**
156
     * Returns a list of all keywords.
157
     *
158
     * @return array
159
     */
160
    public function keywords()
161
    {
162
        $this->assertKeywords();
163
164
        return $this->keywords;
165
    }
166
167
    /**
168
     * Returns all keywords with a given name.
169
     *
170
     * @param string $keyword
171
     *
172
     * @return array
173
     */
174 8
    public function keywordList($keyword)
175
    {
176 8
        $this->assertKeywords();
177
178 8
        if (isset($this->keywords[$keyword])) {
179 8
            return $this->keywords[$keyword];
180
        } else {
181
            return [];
182
        }
183
    }
184
185
    /**
186
     * Checks whether a keyword exists.
187
     *
188
     * @param string $keyword
189
     *
190
     * @return bool
191
     */
192 5
    public function hasKeyword($keyword)
193
    {
194 5
        $this->assertKeywords();
195
196 5
        return isset($this->keywords[$keyword]);
197
    }
198
199
    /**
200
     * Returns the first keyword with a given name.
201
     *
202
     * @param string $keyword
203
     *
204
     * @return object
205
     */
206 5
    public function keyword($keyword)
207
    {
208 5
        $list = $this->keywordList($keyword);
209 5
        if (empty($list)) {
210
            throw new RuntimeException(sprintf('Keyword "%s" does not exist.', $keyword));
211
        }
212
213 5
        return $list[0];
214
    }
215
216
    /**
217
     * Gets all data cells.
218
     *
219
     * @param array
220
     */
221
    public function data()
222
    {
223
        $this->assertData();
224
225
        return $this->data;
226
    }
227
228 9
    private function parseKeywordLine($line)
229
    {
230 9
        $data = new stdClass();
231
232 9
        $line = trim(str_replace('""', ' ', $line));
233 9
        $data->raw = $line;
234
235 9
        $equalPos = self::findQuoted($line, '=');
236 9
        if ($equalPos <= 0) {
237
            return;
238
        }
239
240 9
        $key = substr($line, 0, $equalPos);
241 9
        $data->subKeys = [];
242 9
        if (substr($key, -1) === ')' && ($start = self::findQuotedReverse($key, '(')) !== false) {
243 9
            $data->subKeys = self::split(substr($key, $start + 1, -1));
244 9
            $key = substr($key, 0, $start);
245 9
        }
246 9
        $data->lang = null;
247 9
        if (substr($key, -1) === ']' && ($start = self::findQuotedReverse($key, '[')) !== false) {
248 1
            $data->lang = trim(substr($key, $start + 1, -1), '"');
249 1
            $key = substr($key, 0, $start);
250 1
        }
251
252 9
        $data->values = self::split(substr($line, $equalPos + 1));
253
254 9
        if (!isset($this->keywords[$key])) {
255 9
            $this->keywords[$key] = [];
256 9
        }
257 9
        $this->keywords[$key][] = $data;
258
259 9
        if ($key === 'CHARSET') {
260 9
            $this->charset = $data->values[0];
261 9
            if ($this->charset === 'ANSI' && $this->codepage === null) {
262 9
                $this->codepage = 'ISO-8859-1';
263 9
            }
264 9
        } elseif ($key === 'CODEPAGE') {
265 2
            $this->codepage = $data->values[0];
266 2
        }
267 9
    }
268
269 9
    private function assertKeywords()
270
    {
271 9
        if ($this->keywords !== null) {
272 5
            return;
273
        }
274
275 9
        $this->handle = fopen($this->path, 'r');
276 9
        if ($this->handle === false) {
277
            throw new RuntimeException('Could not open file.');
278
        }
279
280 9
        $this->keywords = [];
281 9
        $remainder = '';
282 9
        while (($line = fgets($this->handle)) !== false) {
283 9
            $line = trim($this->decodeLine($line));
284 9
            if ($line == 'DATA=') {
285 9
                break;
286
            }
287 9
            $remainder .= $line;
288 9
            while (($i = self::findQuoted($remainder, ';')) !== false) {
289 9
                $this->parseKeywordLine(substr($remainder, 0, $i));
290 9
                $remainder = substr($remainder, $i + 1);
291 9
            }
292 9
        }
293
294 9
        $this->dataOffset = ftell($this->handle);
295 9
    }
296
297 2
    private function assertData()
298
    {
299 2
        if ($this->data !== null) {
300 1
            return;
301
        }
302
303 2
        $this->assertKeywords();
304
305 2
        fseek($this->handle, $this->dataOffset, SEEK_SET);
306
307 2
        $raw = '';
308 2
        while (($line = fgets($this->handle)) !== false) {
309 2
            $line = trim($this->decodeLine($line), "\r\n");
310 2
            $raw .= $line;
311 2
        }
312
313 2
        $cells = [];
314 2
        $len = strlen($raw);
315 2
        $value = '';
316 2
        for ($i = 0; $i < $len; $i++) {
317 2
            if ($value === '' && $raw[$i] === '"' && ($end = strpos($raw, '"', $i + 1)) !== false) {
318 2
                $cells[] = substr($raw, $i + 1, $end - $i - 1);
319 2
                $i = $end;
320 2
                $value = '';
321 2
            } elseif (in_array($raw[$i], [' ', ',', ';', "\t"])) {
322 2
                if ($value !== '') {
323 2
                    $cells[] = $value;
324 2
                    $value = '';
325 2
                }
326 2
            } else {
327 2
                $value .= $raw[$i];
328
            }
329 2
        }
330 2
        if ($value !== '') {
331
            $cells[] = $value;
332
        }
333
334 2
        $this->data = $cells;
335
336 2
        fclose($this->handle);
337 2
    }
338
339 2
    private function assertIndexMultipliers()
340
    {
341 2
        if ($this->indexMultipliers !== null) {
342 1
            return;
343
        }
344
345 2
        $variables = $this->variables();
346 2
        $count = count($variables);
347
348 2
        $this->indexMultipliers = [];
349 2
        $this->indexMultipliers[$count - 1] = 1;
350 2
        for ($i = $count - 2; $i >= 0; --$i) {
351 2
            $this->indexMultipliers[$i] = count($this->values($variables[$i + 1])) * $this->indexMultipliers[$i + 1];
352 2
        }
353 2
    }
354
355 9
    private function decodeLine($line)
356
    {
357 9
        if ($this->codepage !== null) {
358 9
            $line = iconv($this->codepage, 'UTF8', $line);
359 9
        } elseif ($this->charset !== 'ANSI') {
360 9
            $line = iconv($this->charset, 'UTF8', $line);
361 9
        }
362
363 9
        return $line;
364
    }
365
366 9
    private static function split($string)
367
    {
368 9
        $values = [];
369 9
        while (($pos = self::findQuoted($string, ',')) !== false) {
370 9
            $values[] = trim(trim(substr($string, 0, $pos)), '"');
371 9
            $string = substr($string, $pos + 1);
372 9
        }
373 9
        $values[] = trim(trim($string), '"');
374
375 9
        return $values;
376
    }
377
378 9
    private static function findQuoted($haystack, $needle)
379
    {
380 9
        $pos = 0;
381 9
        while (($pos = strpos($haystack, $needle, $pos)) !== false) {
382 9
            if (substr_count($haystack, '"', 0, $pos) % 2 == 0) {
383 9
                return $pos;
384
            }
385 9
            $pos++;
386 9
        }
387
388 9
        return $pos;
389
    }
390
391 9
    private static function findQuotedReverse($haystack, $needle)
392
    {
393 9
        $len = strlen($haystack);
394 9
        $pos = strlen($haystack);
395 9
        while (($pos = strrpos($haystack, $needle, $pos - $len)) !== false) {
396 9
            if (substr_count($haystack, '"', $pos) % 2 == 0) {
397 9
                return $pos;
398
            }
399
            $pos--;
400
        }
401
402
        return $pos;
403
    }
404
}
405