Completed
Pull Request — master (#1)
by
unknown
06:11
created

Px::keyword()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4.5923

Importance

Changes 2
Bugs 0 Features 2
Metric Value
c 2
b 0
f 2
dl 0
loc 10
ccs 4
cts 6
cp 0.6667
rs 9.2
cc 4
eloc 6
nc 3
nop 1
crap 4.5923
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 8
    public function __construct($path)
61
    {
62 8
        $this->path = $path;
63 8
        $this->charset = self::DEFAULT_CHARSET;
64 8
    }
65
66
    /**
67
     * Returns a list of all variables.
68
     *
69
     * @return array
70
     */
71 3
    public function variables()
72
    {
73 3
      if(!$this->keyword('STUB')) {
74
        return $this->keyword('HEADING')->values;
75
      }
76 3
      if(!$this->keyword('HEADING')) {
77
        return $this->keyword('STUB')->values;
78
      }
79 3
      return array_merge($this->keyword('STUB')->values, $this->keyword('HEADING')->values);
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 7
    public function keywordList($keyword)
175
    {
176 7
        $this->assertKeywords();
177
178 7
        if (isset($this->keywords[$keyword])) {
179 7
            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 1
    public function hasKeyword($keyword)
193
    {
194 1
        $this->assertKeywords();
195
196 1
        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 4
    public function keyword($keyword)
207
    {
208 4
        $list = $this->keywordList($keyword);
209 4
        if (empty($list)) {
210
            if ($keyword === 'STUB' || $keyword === 'HEADING') { return false; }
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Z38\PcAxis\Px::keyword of type object.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
211
            throw new RuntimeException(sprintf('Keyword "%s" does not exist.', $keyword));
212
        }
213
214 4
        return $list[0];
215
    }
216
217
    /**
218
     * Gets all data cells.
219
     *
220
     * @param array
221
     */
222
    public function data()
223
    {
224
        $this->assertData();
225
226
        return $this->data;
227
    }
228
229 8
    private function parseKeywordLine($line)
230
    {
231 8
        $data = new stdClass();
232
233 8
        $line = trim(str_replace('""', ' ', $line));
234 8
        $data->raw = $line;
235
236 8
        $equalPos = self::findQuoted($line, '=');
237 8
        if ($equalPos <= 0) {
238
            return;
239
        }
240
241 8
        $key = substr($line, 0, $equalPos);
242 8
        $data->subKeys = [];
243 8
        if (substr($key, -1) === ')' && ($start = self::findQuotedReverse($key, '(')) !== false) {
244 8
            $data->subKeys = self::split(substr($key, $start + 1, -1));
245 8
            $key = substr($key, 0, $start);
246 8
        }
247 8
        $data->lang = null;
248 8
        if (substr($key, -1) === ']' && ($start = self::findQuotedReverse($key, '[')) !== false) {
249 1
            $data->lang = trim(substr($key, $start + 1, -1), '"');
250 1
            $key = substr($key, 0, $start);
251 1
        }
252
253 8
        $data->values = self::split(substr($line, $equalPos + 1));
254
255 8
        if (!isset($this->keywords[$key])) {
256 8
            $this->keywords[$key] = [];
257 8
        }
258 8
        $this->keywords[$key][] = $data;
259
260 8
        if ($key === 'CHARSET') {
261 8
            $this->charset = $data->values[0];
262 8
            if ($this->charset === 'ANSI' && $this->codepage === null) {
263 8
                $this->codepage = 'ISO-8859-1';
264 8
            }
265 8
        } elseif ($key === 'CODEPAGE') {
266 1
            $this->codepage = $data->values[0];
267 1
        }
268 8
    }
269
270 8
    private function assertKeywords()
271
    {
272 8
        if ($this->keywords !== null) {
273 4
            return;
274
        }
275
276 8
        $this->handle = fopen($this->path, 'r');
277 8
        if ($this->handle === false) {
278
            throw new RuntimeException('Could not open file.');
279
        }
280
281 8
        $this->keywords = [];
282 8
        $remainder = '';
283 8
        while (($line = fgets($this->handle)) !== false) {
284 8
            $line = trim($this->decodeLine($line));
285 8
            if ($line == 'DATA=') {
286 8
                break;
287
            }
288 8
            $remainder .= $line;
289 8
            while (($i = self::findQuoted($remainder, ';')) !== false) {
290 8
                $this->parseKeywordLine(substr($remainder, 0, $i));
291 8
                $remainder = substr($remainder, $i + 1);
292 8
            }
293 8
        }
294
295 8
        $this->dataOffset = ftell($this->handle);
296 8
    }
297
298 2
    private function assertData()
299
    {
300 2
        if ($this->data !== null) {
301 1
            return;
302
        }
303
304 2
        $this->assertKeywords();
305
306 2
        fseek($this->handle, $this->dataOffset, SEEK_SET);
307
308 2
        $raw = '';
309 2
        while (($line = fgets($this->handle)) !== false) {
310 2
            $line = trim($this->decodeLine($line), "\r\n");
311 2
            $raw .= $line;
312 2
        }
313
314 2
        $cells = [];
315 2
        $len = strlen($raw);
316 2
        $value = '';
317 2
        for ($i = 0; $i < $len; $i++) {
318 2
            if ($value === '' && $raw[$i] === '"' && ($end = strpos($raw, '"', $i + 1)) !== false) {
319 2
                $cells[] = substr($raw, $i + 1, $end - $i - 1);
320 2
                $i = $end;
321 2
                $value = '';
322 2
            } elseif (in_array($raw[$i], [' ', ',', ';', "\t"])) {
323 2
                if ($value !== '') {
324 2
                    $cells[] = $value;
325 2
                    $value = '';
326 2
                }
327 2
            } else {
328 2
                $value .= $raw[$i];
329
            }
330 2
        }
331 2
        if ($value !== '') {
332
            $cells[] = $value;
333
        }
334
335 2
        $this->data = $cells;
336
337 2
        fclose($this->handle);
338 2
    }
339
340 2
    private function assertIndexMultipliers()
341
    {
342 2
        if ($this->indexMultipliers !== null) {
343 1
            return;
344
        }
345
346 2
        $variables = $this->variables();
347 2
        $count = count($variables);
348
349 2
        $this->indexMultipliers = [];
350 2
        $this->indexMultipliers[$count - 1] = 1;
351 2
        for ($i = $count - 2; $i >= 0; --$i) {
352 2
            $this->indexMultipliers[$i] = count($this->values($variables[$i + 1])) * $this->indexMultipliers[$i + 1];
353 2
        }
354 2
    }
355
356 8
    private function decodeLine($line)
357
    {
358 8
        if ($this->codepage !== null) {
359 8
            $line = iconv($this->codepage, 'UTF8', $line);
360 8
        } elseif ($this->charset !== 'ANSI') {
361 8
            $line = iconv($this->charset, 'UTF8', $line);
362 8
        }
363
364 8
        return $line;
365
    }
366
367 8
    private static function split($string)
368
    {
369 8
        $values = [];
370 8
        while (($pos = self::findQuoted($string, ',')) !== false) {
371 8
            $values[] = trim(trim(substr($string, 0, $pos)), '"');
372 8
            $string = substr($string, $pos + 1);
373 8
        }
374 8
        $values[] = trim(trim($string), '"');
375
376 8
        return $values;
377
    }
378
379 8
    private static function findQuoted($haystack, $needle)
380
    {
381 8
        $pos = 0;
382 8
        while (($pos = strpos($haystack, $needle, $pos)) !== false) {
383 8
            if (substr_count($haystack, '"', 0, $pos) % 2 == 0) {
384 8
                return $pos;
385
            }
386 8
            $pos++;
387 8
        }
388
389 8
        return $pos;
390
    }
391
392 8
    private static function findQuotedReverse($haystack, $needle)
393
    {
394 8
        $len = strlen($haystack);
395 8
        $pos = strlen($haystack);
396 8
        while (($pos = strrpos($haystack, $needle, $pos - $len)) !== false) {
397 8
            if (substr_count($haystack, '"', $pos) % 2 == 0) {
398 8
                return $pos;
399
            }
400
            $pos--;
401
        }
402
403
        return $pos;
404
    }
405
}
406