Completed
Push — master ( 03f444...9d4407 )
by ignace nyamagana
02:24
created

Reader::jsonPreserveOffset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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