Completed
Push — master ( 473a0b...a42475 )
by ignace nyamagana
05:17 queued 03:11
created

RecordSet::getIterator()   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 CallbackFilterIterator;
18
use Countable;
19
use DOMDocument;
20
use Generator;
21
use Iterator;
22
use IteratorAggregate;
23
use JsonSerializable;
24
use League\Csv\Exception\InvalidArgumentException;
25
use League\Csv\Exception\RuntimeException;
26
use LimitIterator;
27
28
/**
29
 * A class to manage extracting and filtering a CSV
30
 *
31
 * @package League.csv
32
 * @since   9.0.0
33
 * @author  Ignace Nyamagana Butera <[email protected]>
34
 *
35
 */
36
class RecordSet implements JsonSerializable, IteratorAggregate, Countable
37
{
38
    use ValidatorTrait;
39
40
    /**
41
     * The CSV iterator result
42
     *
43
     * @var Iterator
44
     */
45
    protected $iterator;
46
47
    /**
48
     * The CSV header
49
     *
50
     * @var array
51
     */
52
    protected $column_names = [];
53
54
    /**
55
     * Charset Encoding for the CSV
56
     *
57
     * This information is used when converting the CSV to XML or JSON
58
     *
59
     * @var string
60
     */
61
    protected $conversion_input_encoding = 'UTF-8';
62
63
    /**
64
     * Tell whether the CSV document offset
65
     * must be kept on output
66
     *
67
     * @var bool
68
     */
69
    protected $preserve_offset = false;
70
71
    /**
72
     * New instance
73
     *
74
     * @param Iterator $iterator     a CSV iterator
75
     * @param array    $column_names the CSV header
76
     */
77 114
    public function __construct(Iterator $iterator, array $column_names = [])
78
    {
79 114
        $this->iterator = $iterator;
80 114
        $this->column_names = $column_names;
81 114
    }
82
83
    /**
84
     * @inheritdoc
85
     */
86 114
    public function __destruct()
87
    {
88 114
        $this->iterator = null;
89 114
    }
90
91
    /**
92
     * Returns the field names associate with the RecordSet
93
     *
94
     * @return string[]
95
     */
96 6
    public function getColumnNames(): array
97
    {
98 6
        return $this->column_names;
99
    }
100
101
    /**
102
     * Tell whether the CSV document offset
103
     * must be kept on output
104
     *
105
     * @return bool
106
     */
107 2
    public function isOffsetPreserved(): bool
108
    {
109 2
        return $this->preserve_offset;
110
    }
111
112
    /**
113
     * Returns the conversion input encoding
114
     *
115
     * @return string
116
     */
117 2
    public function getConversionInputEncoding(): string
118
    {
119 2
        return $this->conversion_input_encoding;
120
    }
121
122
    /**
123
     * @inheritdoc
124
     */
125 2
    public function getIterator(): Generator
126
    {
127 2
        return $this->iteratorToGenerator($this->iterator, $this->preserve_offset);
128
    }
129
130
    /**
131
     * Return the generator depending on the preserveOffset setting
132
     *
133
     * @param Iterator $iterator
134
     *
135
     * @return Generator
136
     */
137 14
    protected function iteratorToGenerator(Iterator $iterator, bool $preserve_offset): Generator
138
    {
139 14
        if ($preserve_offset) {
140 4
            foreach ($iterator as $offset => $value) {
141 4
                yield $offset => $value;
142
            }
143 4
            return;
144
        }
145
146 10
        foreach ($iterator as $value) {
147 8
            yield $value;
148
        }
149 6
    }
150
151
    /**
152
     * @inheritdoc
153
     */
154 2
    public function count(): int
155
    {
156 2
        return iterator_count($this->iterator);
157
    }
158
159
    /**
160
     * @inheritdoc
161
     */
162 2
    public function jsonSerialize(): array
163
    {
164 2
        return iterator_to_array($this->convertToUtf8($this->iterator), $this->preserve_offset);
165
    }
166
167
    /**
168
     * Convert Csv file into UTF-8
169
     *
170
     * @param Iterator $iterator
171
     *
172
     * @return Iterator
173
     */
174 8
    protected function convertToUtf8(Iterator $iterator): Iterator
175
    {
176 8
        if (stripos($this->conversion_input_encoding, 'UTF-8') !== false) {
177 6
            return $iterator;
178
        }
179
180
        $walker = function (&$value, &$offset) {
181 2
            $value = mb_convert_encoding((string) $value, 'UTF-8', $this->conversion_input_encoding);
182 2
            $offset = mb_convert_encoding((string) $offset, 'UTF-8', $this->conversion_input_encoding);
183 2
        };
184
185
        $convert = function (array $record) use ($walker): array {
186 2
            array_walk($record, $walker);
187
188 2
            return $record;
189 2
        };
190
191 2
        return new MapIterator($iterator, $convert);
192
    }
193
194
    /**
195
     * Returns a HTML table representation of the CSV Table
196
     *
197
     * @param string $class_attr  optional classname
198
     * @param string $column_attr column attribute name
199
     * @param string $offset_attr offset attribute name
200
     *
201
     * @return string
202
     */
203 4
    public function toHTML(
204
        string $class_attr = 'table-csv-data',
205
        string $column_attr = 'title',
206
        string $offset_attr = 'data-record-offset'
207
    ): string {
208 4
        $doc = $this->toXML('table', 'tr', 'td', $column_attr, $offset_attr);
209 4
        $doc->documentElement->setAttribute('class', $class_attr);
210
211 4
        return $doc->saveHTML($doc->documentElement);
212
    }
213
214
    /**
215
     * Transforms a CSV into a XML
216
     *
217
     * @param string $root_name   XML root node name
218
     * @param string $record_name XML row node name
219
     * @param string $cell_name   XML cell node name
220
     * @param string $column_attr XML column attribute name
221
     * @param string $offset_attr XML offset attribute name
222
     *
223
     * @return DOMDocument
224
     */
225 6
    public function toXML(
226
        string $root_name = 'csv',
227
        string $record_name = 'row',
228
        string $cell_name = 'cell',
229
        string $column_attr = 'name',
230
        string $offset_attr = 'offset'
231
    ): DOMDocument {
232 6
        $encoder = (new XMLEncoder())
233 6
            ->rootName($root_name)
234 6
            ->recordName($record_name)
235 6
            ->itemName($cell_name)
236 6
            ->columnAttributeName($column_attr)
237 6
            ->offsetAttributeName($offset_attr)
238 6
            ->preserveItemOffset(!empty($this->column_names))
239
        ;
240
241 6
        return $encoder->encode($this->convertToUtf8($this->iterator), $this->preserve_offset);
242
    }
243
244
    /**
245
     * Returns a sequential array of all CSV lines
246
     *
247
     * @return array
248
     */
249 64
    public function fetchAll(): array
250
    {
251 64
        return iterator_to_array($this->iterator, $this->preserve_offset);
252
    }
253
254
    /**
255
     * Returns a single row from the CSV
256
     *
257
     * By default if no offset is provided the first row of the CSV is selected
258
     *
259
     * @param int $offset the CSV row offset
260
     *
261
     * @return array
262
     */
263 6
    public function fetchOne(int $offset = 0): array
264
    {
265 6
        $offset = $this->filterInteger($offset, 0, __METHOD__.': the submitted offset is invalid');
266 4
        $it = new LimitIterator($this->iterator, $offset, 1);
267 4
        $it->rewind();
268
269 4
        return (array) $it->current();
270
    }
271
272
    /**
273
     * Returns the next value from a single CSV column
274
     *
275
     * By default if no column index is provided the first column of the CSV is selected
276
     *
277
     * @param string|int $index CSV column index
278
     *
279
     * @return Generator
280
     */
281 18
    public function fetchColumn($index = 0): Generator
282
    {
283 18
        $offset = $this->getColumnIndex($index, __METHOD__.': the column index `%s` value is invalid');
284
        $filter = function (array $record) use ($offset): bool {
285 12
            return isset($record[$offset]);
286 12
        };
287
288
        $select = function (array $record) use ($offset): string {
289 10
            return $record[$offset];
290 12
        };
291
292 12
        $iterator = new MapIterator(new CallbackFilterIterator($this->iterator, $filter), $select);
293
294 12
        return $this->iteratorToGenerator($iterator, $this->preserve_offset);
295
    }
296
297
    /**
298
     * Filter a column name against the CSV header if any
299
     *
300
     * @param string|int $field         the field name or the field index
301
     * @param string     $error_message the associated error message
302
     *
303
     * @throws InvalidArgumentException if the field is invalid
304
     * @throws RuntimeException         if the column is not found
305
     *
306
     * @return string|int
307
     */
308 26
    protected function getColumnIndex($field, string $error_message)
309
    {
310 26
        if (false !== array_search($field, $this->column_names, true)) {
311 2
            return $field;
312
        }
313
314 24
        if (is_string($field)) {
315 2
            throw new InvalidArgumentException(sprintf($error_message, $field));
316
        }
317
318 22
        $index = $this->filterInteger($field, 0, $error_message);
319 20
        if (empty($this->column_names)) {
320 16
            return $index;
321
        }
322
323 4
        $index = array_search($index, array_flip($this->column_names), true);
324 4
        if (false !== $index) {
325 2
            return $index;
326
        }
327
328 2
        throw new RuntimeException(sprintf($error_message, $field));
329
    }
330
331
    /**
332
     * Fetches the next key-value pairs from a result set (first
333
     * column is the key, second column is the value).
334
     *
335
     * By default if no column index is provided:
336
     * - the first CSV column is used to provide the keys
337
     * - the second CSV column is used to provide the value
338
     *
339
     * @param string|int $offset_index The column index to serve as offset
340
     * @param string|int $value_index  The column index to serve as value
341
     *
342
     * @return Generator
343
     */
344 8
    public function fetchPairs($offset_index = 0, $value_index = 1): Generator
345
    {
346 8
        $offset = $this->getColumnIndex($offset_index, __METHOD__.': the offset index value is invalid');
347 8
        $value = $this->getColumnIndex($value_index, __METHOD__.': the value index value is invalid');
348
349
        $filter = function (array $record) use ($offset): bool {
350 8
            return isset($record[$offset]);
351 8
        };
352
353 8
        $select = function (array $record) use ($offset, $value): array {
354 6
            return [$record[$offset], $record[$value] ?? null];
355 8
        };
356
357 8
        $iterator = new MapIterator(new CallbackFilterIterator($this->iterator, $filter), $select);
358
359 8
        foreach ($iterator as $pair) {
360 6
            yield $pair[0] => $pair[1];
361
        }
362 8
    }
363
364
    /**
365
     * Sets the CSV encoding charset
366
     *
367
     * @param string $str
368
     *
369
     * @throws InvalidArgumentException if the charset is empty
370
     *
371
     * @return static
372
     */
373 4
    public function setConversionInputEncoding(string $str): self
374
    {
375 4
        $str = str_replace('_', '-', $str);
376 4
        $str = filter_var($str, FILTER_SANITIZE_STRING, ['flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH]);
377 4
        $str = trim($str);
378 4
        if ('' === $str) {
379 2
            throw new InvalidArgumentException('you should use a valid charset');
380
        }
381 2
        $this->conversion_input_encoding = strtoupper($str);
382
383 2
        return $this;
384
    }
385
386
    /**
387
     * Whether we should preserve the CSV document record offset.
388
     *
389
     * If set to true CSV document record offset will added to
390
     * method output where it makes sense.
391
     *
392
     * @param bool $status
393
     *
394
     * @return static
395
     */
396 8
    public function preserveOffset(bool $status)
397
    {
398 8
        $this->preserve_offset = $status;
399
400 8
        return $this;
401
    }
402
}
403