Completed
Push — master ( 764589...92f7d6 )
by ignace nyamagana
04:15
created

Reader::setHeader()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 8.2464

Importance

Changes 0
Metric Value
cc 7
eloc 18
nc 6
nop 1
dl 0
loc 28
ccs 12
cts 17
cp 0.7059
crap 8.2464
rs 6.7272
c 0
b 0
f 0
1
<?php
2
/**
3
* This file is part of the League.csv library
4
*
5
* @license http://opensource.org/licenses/MIT
6
* @link https://github.com/thephpleague/csv/
7
* @version 9.1.1
8
* @package League.csv
9
*
10
* For the full copyright and license information, please view the LICENSE
11
* file that was distributed with this source code.
12
*/
13
declare(strict_types=1);
14
15
namespace League\Csv;
16
17
use BadMethodCallException;
18
use CallbackFilterIterator;
19
use Countable;
20
use Iterator;
21
use IteratorAggregate;
22
use JsonSerializable;
23
use SplFileObject;
24
use TypeError;
25
26
/**
27
 * A class to select records from a CSV document
28
 *
29
 * @package League.csv
30
 * @since  3.0.0
31
 *
32
 * @method array fetchOne(int $nth_record = 0) Returns a single record from the CSV
33
 * @method Generator fetchColumn(string|int $column_index) Returns the next value from a single CSV record field
34
 * @method Generator fetchPairs(string|int $offset_index = 0, string|int $value_index = 1) Fetches the next key-value pairs from the CSV document
35
 */
36
class Reader extends AbstractCsv implements Countable, IteratorAggregate, JsonSerializable
37
{
38
    /**
39
     * header offset
40
     *
41
     * @var int|null
42
     */
43
    protected $header_offset;
44
45
    /**
46
     * header record
47
     *
48
     * @var string[]
49
     */
50
    protected $header = [];
51
52
    /**
53
     * records count
54
     *
55
     * @var int
56
     */
57
    protected $nb_records = -1;
58
59
    /**
60
     * {@inheritdoc}
61
     */
62
    protected $stream_filter_mode = STREAM_FILTER_READ;
63
64
    /**
65
     * {@inheritdoc}
66
     */
67 2
    public static function createFromPath(string $path, string $open_mode = 'r', $context = null): AbstractCsv
68
    {
69 2
        return new static(Stream::createFromPath($path, $open_mode, $context));
70
    }
71
72
    /**
73
     * Returns the header offset
74
     *
75
     * If no CSV header offset is set this method MUST return null
76
     *
77
     * @return int|null
78
     */
79 10
    public function getHeaderOffset()
80
    {
81 10
        return $this->header_offset;
82
    }
83
84
    /**
85
     * Returns the CSV record used as header
86
     *
87
     * The returned header is represented as an array of string values
88
     *
89
     * @return string[]
90
     */
91 10
    public function getHeader(): array
92
    {
93 10
        if (null === $this->header_offset) {
94 8
            return $this->header;
95
        }
96
97 4
        if (!empty($this->header)) {
98 2
            return $this->header;
99
        }
100
101 4
        $this->header = $this->setHeader($this->header_offset);
102
103 4
        return $this->header;
104
    }
105
106
    /**
107
     * Determine the CSV record header
108
     *
109
     * @param int $offset
110
     *
111
     * @throws Exception If the header offset is set and no record is found or is the empty array
112
     *
113
     * @return string[]
114
     */
115 8
    protected function setHeader(int $offset): array
116
    {
117 8
        $header = [];
0 ignored issues
show
Unused Code introduced by
$header is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
118 8
        $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
119 8
        $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
120 8
        if ($this->document instanceof Stream || PHP_VERSION_ID < 70200) {
121 8
            $this->document->seek($offset);
122 8
            $header = $this->document->current();
123
        } else {
124
            $stream->rewind();
0 ignored issues
show
Bug introduced by
The variable $stream does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
125
            while ($offset !== $stream->key() && $stream->valid()) {
126
                $stream->current();
127
                $stream->next();
128
            }
129
130
            $header = $stream->current();
131
        }
132
133 8
        if (empty($header)) {
134 4
            throw new Exception(sprintf('The header record does not exist or is empty at offset: `%s`', $offset));
135
        }
136
137 4
        if (0 === $offset) {
138 2
            return $this->removeBOM($header, mb_strlen($this->getInputBOM()), $this->enclosure);
139
        }
140
141 2
        return $header;
142
    }
143
144
    /**
145
     * Strip the BOM sequence from a record
146
     *
147
     * @param string[] $record
148
     * @param int      $bom_length
149
     * @param string   $enclosure
150
     *
151
     * @return string[]
152
     */
153 8
    protected function removeBOM(array $record, int $bom_length, string $enclosure): array
154
    {
155 8
        if (0 == $bom_length) {
156 2
            return $record;
157
        }
158
159 6
        $record[0] = mb_substr($record[0], $bom_length);
160 6
        if ($enclosure.$enclosure != substr($record[0].$record[0], strlen($record[0]) - 1, 2)) {
161 4
            return $record;
162
        }
163
164 2
        $record[0] = substr($record[0], 1, -1);
165
166 2
        return $record;
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172 6
    public function __call($method, array $arguments)
173
    {
174 6
        static $whitelisted = ['fetchColumn' => 1, 'fetchOne' => 1, 'fetchPairs' => 1];
175 6
        if (isset($whitelisted[$method])) {
176 2
            return (new ResultSet($this->getRecords(), $this->getHeader()))->$method(...$arguments);
177
        }
178
179 4
        throw new BadMethodCallException(sprintf('%s::%s() method does not exist', __CLASS__, $method));
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185 2
    public function count(): int
186
    {
187 2
        if (-1 === $this->nb_records) {
188 2
            $this->nb_records = iterator_count($this->getRecords());
189
        }
190
191 2
        return $this->nb_records;
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197 2
    public function getIterator(): Iterator
198
    {
199 2
        return $this->getRecords();
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205 2
    public function jsonSerialize(): array
206
    {
207 2
        return iterator_to_array($this->getRecords(), false);
208
    }
209
210
    /**
211
     * Returns the CSV records as an iterator object.
212
     *
213
     * Each CSV record is represented as a simple array containig strings or null values.
214
     *
215
     * If the CSV document has a header record then each record is combined
216
     * to the header record and the header record is removed from the iterator.
217
     *
218
     * If the CSV document is inconsistent. Missing record fields are
219
     * filled with null values while extra record fields are strip from
220
     * the returned object.
221
     *
222
     * @param string[] $header an optional header to use instead of the CSV document header
223
     *
224
     * @return Iterator
225
     */
226 12
    public function getRecords(array $header = []): Iterator
227
    {
228 12
        $header = $this->computeHeader($header);
229 10
        $normalized = function ($record): bool {
230 10
            return is_array($record) && $record != [null];
231 10
        };
232 10
        $bom = $this->getInputBOM();
233 10
        $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
234 10
        $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
235
236 10
        $records = $this->stripBOM(new CallbackFilterIterator($this->document, $normalized), $bom);
237 10
        if (null !== $this->header_offset) {
238 4
            $records = new CallbackFilterIterator($records, function (array $record, int $offset): bool {
239 4
                return $offset !== $this->header_offset;
240 4
            });
241
        }
242
243 10
        return $this->combineHeader($records, $header);
244
    }
245
246
    /**
247
     * Returns the header to be used for iteration
248
     *
249
     * @param string[] $header
250
     *
251
     * @throws Exception If the header contains non unique column name
252
     *
253
     * @return string[]
254
     */
255 16
    protected function computeHeader(array $header)
256
    {
257 16
        if (empty($header)) {
258 14
            $header = $this->getHeader();
259
        }
260
261 16
        if ($header === array_unique(array_filter($header, 'is_string'))) {
262 14
            return $header;
263
        }
264
265 2
        throw new Exception('The header record must be empty or a flat array with unique string values');
266
    }
267
268
    /**
269
     * Combine the CSV header to each record if present
270
     *
271
     * @param Iterator $iterator
272
     * @param string[] $header
273
     *
274
     * @return Iterator
275
     */
276 20
    protected function combineHeader(Iterator $iterator, array $header): Iterator
277
    {
278 20
        if (empty($header)) {
279 14
            return $iterator;
280
        }
281
282 8
        $field_count = count($header);
283 8
        $mapper = function (array $record) use ($header, $field_count): array {
284 8
            if (count($record) != $field_count) {
285 4
                $record = array_slice(array_pad($record, $field_count, null), 0, $field_count);
286
            }
287
288 8
            return array_combine($header, $record);
289 8
        };
290
291 8
        return new MapIterator($iterator, $mapper);
292
    }
293
294
    /**
295
     * Strip the BOM sequence from the returned records if necessary
296
     *
297
     * @param Iterator $iterator
298
     * @param string   $bom
299
     *
300
     * @return Iterator
301
     */
302 16
    protected function stripBOM(Iterator $iterator, string $bom): Iterator
303
    {
304 16
        if ('' === $bom) {
305 10
            return $iterator;
306
        }
307
308 6
        $bom_length = mb_strlen($bom);
309 6
        $mapper = function (array $record, int $index) use ($bom_length): array {
310 6
            if (0 != $index) {
311 2
                return $record;
312
            }
313
314 6
            return $this->removeBOM($record, $bom_length, $this->enclosure);
315 6
        };
316
317 6
        return new MapIterator($iterator, $mapper);
318
    }
319
320
    /**
321
     * Selects the record to be used as the CSV header
322
     *
323
     * Because the header is represented as an array, to be valid
324
     * a header MUST contain only unique string value.
325
     *
326
     * @param int|null $offset the header record offset
327
     *
328
     * @throws Exception if the offset is a negative integer
329
     *
330
     * @return static
331
     */
332 14
    public function setHeaderOffset($offset): self
333
    {
334 14
        if ($offset === $this->header_offset) {
335 8
            return $this;
336
        }
337
338 6
        if (!is_nullable_int($offset)) {
339 2
            throw new TypeError(sprintf(__METHOD__.'() expects 1 Argument to be null or an integer %s given', gettype($offset)));
340
        }
341
342 4
        if (null !== $offset && 0 > $offset) {
343 2
            throw new Exception(__METHOD__.'() expects 1 Argument to be greater or equal to 0');
344
        }
345
346 2
        $this->header_offset = $offset;
347 2
        $this->resetProperties();
348
349 2
        return $this;
350
    }
351
352
    /**
353
     * {@inheritdoc}
354
     */
355 8
    protected function resetProperties()
356
    {
357 8
        $this->nb_records = -1;
358 8
        $this->header = [];
359 8
    }
360
}
361