Completed
Push — master ( a0bcff...a2ba0d )
by z38
02:24
created

Px   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 370
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 89.94%

Importance

Changes 3
Bugs 0 Features 1
Metric Value
wmc 57
c 3
b 0
f 1
lcom 1
cbo 0
dl 0
loc 370
ccs 152
cts 169
cp 0.8994
rs 6.433

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A variables() 0 4 1
A values() 0 9 3
A codes() 0 10 3
A index() 0 14 2
A datum() 0 11 2
A keywords() 0 6 1
A keywordList() 0 10 2
A hasKeyword() 0 6 1
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 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
     * Constructor
52
     *
53
     * @param string $path path to your PX file
54
     */
55 8
    public function __construct($path)
56
    {
57 8
        $this->path = $path;
58 8
        $this->charset = self::DEFAULT_CHARSET;
59 8
    }
60
61
    /**
62
     * Returns a list of all variables.
63
     *
64
     * @return array
65
     */
66 3
    public function variables()
67
    {
68 3
        return array_merge($this->keyword('STUB')->values, $this->keyword('HEADING')->values);
69
    }
70
71
    /**
72
     * Returns a list of all possible values of a variable.
73
     *
74
     * @param string $variable
75
     *
76
     * @return array
77
     */
78 3
    public function values($variable)
79
    {
80 3
        foreach ($this->keywordList('VALUES') as $keyword) {
81 3
            if ($keyword->subKeys[0] == $variable) {
82 3
                return $keyword->values;
83
            }
84 3
        }
85
        throw new RuntimeException(sprintf('Could not determine values of "%s".', $variable));
86
    }
87
88
    /**
89
     * Returns a list of all possible codes of a variable.
90
     *
91
     * @param string $variable
92
     *
93
     * @return array|null
94
     */
95 2
    public function codes($variable)
96
    {
97 2
        foreach ($this->keywordList('CODES') as $keyword) {
98 2
            if ($keyword->subKeys[0] == $variable) {
99 1
                return $keyword->values;
100
            }
101 2
        }
102
103 1
        return null;
104
    }
105
106
    /**
107
     * Computes the index within the data matrix.
108
     *
109
     * @param array $indices An array of all value indices
110
     *
111
     * @return int
112
     */
113 2
    public function index($indices)
114
    {
115 2
        $px = $this;
116 2
        $counts = array_map(function ($variable) use ($px) {
117 2
            return count($px->values($variable));
118 2
        }, $this->variables());
119
120 2
        $index = 0;
121 2
        for ($i = 0; $i < count($indices); ++$i) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
Performance Best Practice introduced by
Consider avoiding function calls on each iteration of the for loop.

If you have a function call in the test part of a for loop, this function is executed on each iteration. Often such a function, can be moved to the initialization part and be cached.

// count() is called on each iteration
for ($i=0; $i < count($collection); $i++) { }

// count() is only called once
for ($i=0, $c=count($collection); $i<$c; $i++) { }
Loading history...
122 2
            $index += $indices[$i] * array_product(array_slice($counts, $i + 1));
123 2
        }
124
125 2
        return $index;
126
    }
127
128
    /**
129
     * Gets a single data point.
130
     *
131
     * @param array $indices An array of all value indices
132
     *
133
     * @return string
134
     */
135 2
    public function datum($indices)
136
    {
137 2
        $this->assertData();
138
139 2
        $index = $this->index($indices);
140 2
        if (isset($this->data[$index])) {
141 2
            return $this->data[$index];
142
        } else {
143
            return null;
144
        }
145
    }
146
147
    /**
148
     * Returns a list of all keywords.
149
     *
150
     * @return array
151
     */
152
    public function keywords()
153
    {
154
        $this->assertKeywords();
155
156
        return $this->keywords;
157
    }
158
159
    /**
160
     * Returns all keywords with a given name.
161
     *
162
     * @param string $keyword
163
     *
164
     * @return array
165
     */
166 7
    public function keywordList($keyword)
167
    {
168 7
        $this->assertKeywords();
169
170 7
        if (isset($this->keywords[$keyword])) {
171 7
            return $this->keywords[$keyword];
172
        } else {
173
            return [];
174
        }
175
    }
176
177
    /**
178
     * Checks whether a keyword exists.
179
     *
180
     * @param string $keyword
181
     *
182
     * @return bool
183
     */
184 1
    public function hasKeyword($keyword)
185
    {
186 1
        $this->assertKeywords();
187
188 1
        return isset($this->keywords[$keyword]);
189
    }
190
191
    /**
192
     * Returns the first keyword with a given name.
193
     *
194
     * @param string $keyword
195
     *
196
     * @return object
197
     */
198 4
    public function keyword($keyword)
199
    {
200 4
        $list = $this->keywordList($keyword);
201 4
        if (!$list) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $list 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...
202
            throw new RuntimeException(sprintf('Keyword "%s" does not exist.', $keyword));
203
        }
204
205 4
        return $list[0];
206
    }
207
208
    /**
209
     * Gets all data cells.
210
     *
211
     * @param array
212
     */
213
    public function data()
214
    {
215
        $this->assertData();
216
217
        return $this->data;
218
    }
219
220 8
    private function parseKeywordLine($line)
221
    {
222 8
        $data = new stdClass();
223
224 8
        $line = trim(str_replace('""', ' ', $line));
225 8
        $data->raw = $line;
226
227 8
        $equalPos = self::findQuoted($line, '=');
228 8
        if ($equalPos <= 0) {
229
            return;
230
        }
231
232 8
        $key = substr($line, 0, $equalPos);
233 8
        $data->subKeys = [];
234 8
        if (substr($key, -1) == ')' && ($start = self::findQuotedReverse($key, '(')) !== false) {
235 8
            $data->subKeys = self::split(substr($key, $start + 1, -1));
236 8
            $key = substr($key, 0, $start);
237 8
        }
238 8
        $data->lang = null;
239 8
        if (substr($key, -1) == ']' && ($start = self::findQuotedReverse($key, '[')) !== false) {
240 1
            $data->lang = trim(substr($key, $start + 1, -1), '"');
241 1
            $key = substr($key, 0, $start);
242 1
        }
243
244 8
        $data->values = self::split(substr($line, $equalPos + 1));
245
246 8
        if (!isset($this->keywords[$key])) {
247 8
            $this->keywords[$key] = [];
248 8
        }
249 8
        $this->keywords[$key][] = $data;
250
251 8
        if ($key === 'CHARSET') {
252 8
            $this->charset = $data->values[0];
253 8
            if ($this->charset === 'ANSI' && $this->codepage === null) {
254 8
                $this->codepage = 'ISO-8859-1';
255 8
            }
256 8
        } elseif ($key === 'CODEPAGE') {
257 1
            $this->codepage = $data->values[0];
258 1
        }
259 8
    }
260
261 8
    private function assertKeywords()
262
    {
263 8
        if ($this->keywords !== null) {
264 4
            return;
265
        }
266
267 8
        $this->handle = fopen($this->path, 'r');
268 8
        if ($this->handle === false) {
269
            throw new RuntimeException('Could not open file.');
270
        }
271
272 8
        $this->keywords = [];
273 8
        $remainder = '';
274 8
        while (($line = fgets($this->handle)) !== false) {
275 8
            $line = trim($this->decodeLine($line));
276 8
            if ($line == 'DATA=') {
277 8
                break;
278
            }
279 8
            $remainder .= $line;
280 8
            while (($i = self::findQuoted($remainder, ';')) !== false) {
281 8
                $this->parseKeywordLine(substr($remainder, 0, $i));
282 8
                $remainder = substr($remainder, $i + 1);
283 8
            }
284 8
        }
285
286 8
        $this->dataOffset = ftell($this->handle);
287 8
    }
288
289 2
    private function assertData()
290
    {
291 2
        if ($this->data !== null) {
292 1
            return;
293
        }
294
295 2
        $this->assertKeywords();
296
297 2
        fseek($this->handle, $this->dataOffset, SEEK_SET);
298
299 2
        $raw = '';
300 2
        while (($line = fgets($this->handle)) !== false) {
301 2
            $line = trim($this->decodeLine($line), "\r\n");
302 2
            $raw .= $line;
303 2
        }
304
305 2
        $cells = [];
306 2
        $len = strlen($raw);
307 2
        $value = '';
308 2
        for ($i = 0;$i < $len;$i++) {
309 2
            if (!$value && $raw[$i] == '"' && ($end = strpos($raw, '"', $i + 1)) !== false) {
310 2
                $cells[] = substr($raw, $i + 1, $end - $i - 1);
311 2
                $i = $end;
312 2
                $value = '';
313 2
            } elseif (in_array($raw[$i], [' ', ',', ';', "\t"])) {
314 2
                if ($value) {
315 2
                    $cells[] = $value;
316 2
                    $value = '';
317 2
                }
318 2
            } else {
319 2
                $value .= $raw[$i];
320
            }
321 2
        }
322 2
        if ($value) {
323
            $cells[] = $value;
324
        }
325
326 2
        $this->data = $cells;
327
328 2
        fclose($this->handle);
329 2
    }
330
331 8
    private function decodeLine($line)
332
    {
333 8
        if ($this->codepage !== null) {
334 8
            $line = iconv($this->codepage, 'UTF8', $line);
335 8
        } elseif ($this->charset !== 'ANSI') {
336 8
            $line = iconv($this->charset, 'UTF8', $line);
337 8
        }
338
339 8
        return $line;
340
    }
341
342 8
    private static function split($string)
343
    {
344 8
        $values = [];
345 8
        while (($pos = self::findQuoted($string, ',')) !== false) {
346 8
            $values[] = trim(trim(substr($string, 0, $pos)), '"');
347 8
            $string = substr($string, $pos + 1);
348 8
        }
349 8
        $values[] = trim(trim($string), '"');
350
351 8
        return $values;
352
    }
353
354 8
    private static function findQuoted($haystack, $needle)
355
    {
356 8
        $pos = 0;
357 8
        while (($pos = strpos($haystack, $needle, $pos)) !== false) {
358 8
            if (substr_count($haystack, '"', 0, $pos) % 2 == 0) {
359 8
                return $pos;
360
            }
361 8
            $pos++;
362 8
        }
363
364 8
        return $pos;
365
    }
366
367 8
    private static function findQuotedReverse($haystack, $needle)
368
    {
369 8
        $len = strlen($haystack);
370 8
        $pos = strlen($haystack);
371 8
        while (($pos = strrpos($haystack, $needle, $pos - $len)) !== false) {
372 8
            if (substr_count($haystack, '"', $pos) % 2 == 0) {
373 8
                return $pos;
374
            }
375
            $pos--;
376
        }
377
378
        return $pos;
379
    }
380
}
381