Completed
Push — master ( 2fe9a9...245e94 )
by ignace nyamagana
04:17 queued 02:57
created

Reader::getRecordPaddingValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
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.0.0
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 League\Csv\Exception\RuntimeException;
24
use SplFileObject;
25
26
/**
27
 * A class to manage records selection from a CSV document
28
 *
29
 * @package League.csv
30
 * @since  3.0.0
31
 *
32
 * @method array fetchAll() Returns a sequential array of all CSV records
33
 * @method array fetchOne(int $offset = 0) Returns a single record from the CSV
34
 * @method Generator fetchColumn(string|int $column_index) Returns the next value from a single CSV record field
35
 * @method Generator fetchPairs(string|int $offset_index, string|int $value_index) Fetches the next key-value pairs from the CSV document
36
 */
37
class Reader extends AbstractCsv implements Countable, IteratorAggregate, JsonSerializable
38
{
39
    /**
40
     * @inheritdoc
41
     */
42
    protected $stream_filter_mode = STREAM_FILTER_READ;
43
44
    /**
45
     * CSV Document header offset
46
     *
47
     * @var int|null
48
     */
49
    protected $header_offset;
50
51
    /**
52
     * CSV Document Header record
53
     *
54
     * @var string[]
55
     */
56
    protected $header = [];
57
58
    /**
59
     * Records count
60
     *
61
     * @var int
62
     */
63
    protected $nb_records = -1;
64
65
    /**
66
     * Returns the record offset used as header
67
     *
68
     * If no CSV record is used this method MUST return null
69
     *
70
     * @return int|null
71
     */
72 10
    public function getHeaderOffset()
73
    {
74 10
        return $this->header_offset;
75
    }
76
77
    /**
78
     * Returns the CSV record header
79
     *
80
     * The returned header is represented as an array of string values
81
     *
82
     * @return string[]
83
     */
84 10
    public function getHeader(): array
85
    {
86 10
        if (null === $this->header_offset) {
87 8
            return $this->header;
88
        }
89
90 4
        if (empty($this->header)) {
91 4
            $this->header = $this->setHeader($this->header_offset);
92
        }
93
94 4
        return $this->header;
95
    }
96
97
    /**
98
     * Determine the CSV record header
99
     *
100
     * @param int $offset
101
     *
102
     * @throws RuntimeException If the header offset is an integer
103
     *                          and the corresponding record is missing
104
     *                          or is an empty array
105
     *
106
     * @return string[]
107
     */
108 6
    protected function setHeader(int $offset): array
109
    {
110 6
        $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
111 6
        $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
112 6
        $this->document->seek($offset);
113 6
        $header = $this->document->current();
114 6
        if (empty($header)) {
115 2
            throw new RuntimeException(sprintf('The header record does not exist or is empty at offset: `%s`', $offset));
116
        }
117
118 4
        if (0 === $offset) {
119 2
            return $this->removeBOM($header, mb_strlen($this->getInputBOM()), $this->enclosure);
120
        }
121
122 2
        return $header;
123
    }
124
125
    /**
126
     * Strip the BOM sequence from a record
127
     *
128
     * @param string[] $record
129
     * @param int      $bom_length
130
     * @param string   $enclosure
131
     *
132
     * @return string[]
133
     */
134 8
    protected function removeBOM(array $record, int $bom_length, string $enclosure): array
135
    {
136 8
        if (0 == $bom_length) {
137 2
            return $record;
138
        }
139
140 6
        $record[0] = mb_substr($record[0], $bom_length);
141 6
        if ($enclosure == mb_substr($record[0], 0, 1) && $enclosure == mb_substr($record[0], -1, 1)) {
142 2
            $record[0] = mb_substr($record[0], 1, -1);
143
        }
144
145 6
        return $record;
146
    }
147
148
    /**
149
     * @inheritdoc
150
     */
151 6
    public function __call($method, array $arguments)
152
    {
153 6
        $whitelisted = ['fetchColumn' => 1, 'fetchPairs' => 1, 'fetchOne' => 1, 'fetchAll' => 1];
154 6
        if (isset($whitelisted[$method])) {
155 2
            return (new ResultSet($this->getRecords(), $this->getHeader()))->$method(...$arguments);
156
        }
157
158 4
        throw new BadMethodCallException(sprintf('%s::%s() method does not exist', __CLASS__, $method));
159
    }
160
161
    /**
162
     * @inheritdoc
163
     */
164 2
    public function count(): int
165
    {
166 2
        if (-1 === $this->nb_records) {
167 2
            $this->nb_records = iterator_count($this->getRecords());
168
        }
169
170 2
        return $this->nb_records;
171
    }
172
173
    /**
174
     * @inheritdoc
175
     */
176 2
    public function getIterator(): Iterator
177
    {
178 2
        return $this->getRecords();
179
    }
180
181
    /**
182
     * @inheritdoc
183
     */
184 2
    public function jsonSerialize(): array
185
    {
186 2
        return iterator_to_array($this->getRecords(), false);
187
    }
188
189
    /**
190
     * Returns the CSV records in an iterator object.
191
     *
192
     * Each CSV record is represented as a simple array of string or null values.
193
     *
194
     * If the CSV document has a header record then each record is combined
195
     * to each header record and the header record is removed from the iterator.
196
     *
197
     * If the CSV document is inconsistent. Missing record fields are
198
     * filled with null values while extra record fields are strip from
199
     * the returned object.
200
     *
201
     * @param string[] $header an optional header to use instead of the CSV document header
202
     *
203
     * @return Iterator
204
     */
205 12
    public function getRecords(array $header = []): Iterator
206
    {
207 12
        $header = $this->computeHeader($header);
208
        $normalized = function ($record): bool {
209 10
            return is_array($record) && $record != [null];
210 5
        };
211 10
        $bom = $this->getInputBOM();
212 10
        $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
213 10
        $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
214
215 10
        $records = $this->stripBOM(new CallbackFilterIterator($this->document, $normalized), $bom);
216 10
        if (null !== $this->header_offset) {
217
            $records = new CallbackFilterIterator($records, function (array $record, int $offset): bool {
218 4
                return $offset !== $this->header_offset;
219 4
            });
220
        }
221
222 10
        return $this->combineHeader($records, $header);
223
    }
224
225
    /**
226
     * Returns the header to be used for iteration
227
     *
228
     * @param string[] $header
229
     *
230
     * @throws RuntimeException If the header contains non unique column name
231
     *
232
     * @return string[]
233
     */
234 16
    protected function computeHeader(array $header)
235
    {
236 16
        if (empty($header)) {
237 14
            $header = $this->getHeader();
238
        }
239
240 16
        if ($header === array_unique(array_filter($header, 'is_string'))) {
241 14
            return $header;
242
        }
243
244 2
        throw new RuntimeException('The header record must be empty or a flat array with unique string values');
245
    }
246
247
    /**
248
     * Add the CSV header if present and valid
249
     *
250
     * @param Iterator $iterator
251
     * @param string[] $header
252
     *
253
     * @return Iterator
254
     */
255 20
    protected function combineHeader(Iterator $iterator, array $header): Iterator
256
    {
257 20
        if (empty($header)) {
258 14
            return $iterator;
259
        }
260
261 8
        $field_count = count($header);
262
        $mapper = function (array $record) use ($header, $field_count): array {
263 8
            if (count($record) != $field_count) {
264 4
                $record = array_slice(array_pad($record, $field_count, null), 0, $field_count);
265
            }
266
267 8
            return array_combine($header, $record);
268 8
        };
269
270 8
        return new MapIterator($iterator, $mapper);
271
    }
272
273
    /**
274
     * Strip the BOM sequence if present
275
     *
276
     * @param Iterator $iterator
277
     * @param string   $bom
278
     *
279
     * @return Iterator
280
     */
281 16
    protected function stripBOM(Iterator $iterator, string $bom): Iterator
282
    {
283 16
        if ('' === $bom) {
284 10
            return $iterator;
285
        }
286
287 6
        $bom_length = mb_strlen($bom);
288 6
        $mapper = function (array $record, int $index) use ($bom_length): array {
289 6
            if (0 != $index) {
290 2
                return $record;
291
            }
292
293 6
            return $this->removeBOM($record, $bom_length, $this->enclosure);
294 6
        };
295
296 6
        return new MapIterator($iterator, $mapper);
297
    }
298
299
    /**
300
     * Selects the record to be used as the CSV header
301
     *
302
     * Because of the header is represented as an array, to be valid
303
     * a header MUST contain only unique string value.
304
     *
305
     * @param int|null $offset the header record offset
306
     *
307
     * @return static
308
     */
309 10
    public function setHeaderOffset($offset): self
310
    {
311 10
        if (null !== $offset) {
312 2
            $offset = $this->filterMinRange($offset, 0, __METHOD__.'() expects the header offset index to be a positive integer or 0, %s given');
313
        }
314
315 10
        if ($offset !== $this->header_offset) {
316 2
            $this->header_offset = $offset;
317 2
            $this->resetProperties();
318
        }
319
320 10
        return $this;
321
    }
322
323
    /**
324
     * @inheritdoc
325
     */
326 8
    protected function resetProperties()
327
    {
328 8
        $this->nb_records = -1;
329 8
        $this->header = [];
330 8
    }
331
}
332