Completed
Push — master ( 4c829a...436317 )
by ignace nyamagana
08:53 queued 07:35
created

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