Completed
Push — master ( 6ca7fe...652926 )
by ignace nyamagana
03:13 queued 02:01
created

Reader   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 293
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
dl 0
loc 293
ccs 85
cts 85
cp 1
rs 9.6
c 0
b 0
f 0
wmc 32
lcom 1
cbo 4

14 Methods

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