Completed
Push — master ( 370f1d...15957b )
by ignace nyamagana
05:47 queued 03:47
created

RecordSet::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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