Completed
Pull Request — master (#210)
by ignace nyamagana
12:18
created

RecordSet::toDOMNode()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 25
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 12
cts 12
cp 1
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 20
nc 6
nop 7
crap 4
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 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 108
    public function __construct(Iterator $iterator, array $column_names = [])
78
    {
79 108
        $this->iterator = $iterator;
80 108
        $this->column_names = $column_names;
81 108
    }
82
83
    /**
84
     * @inheritdoc
85
     */
86 108
    public function __destruct()
87
    {
88 108
        $this->iterator = null;
89 108
    }
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
     * Returns a specific field names according to its offset
103
     *
104
     * If no field name is found or associated to the submitted
105
     * offset an empty string is returned
106
     *
107
     * @param int $offset
108
     *
109
     * @return string
110
     */
111 4
    public function getColumnName(int $offset): string
112
    {
113 4
        return $this->column_names[$offset] ?? '';
114
    }
115
116
    /**
117
     * @inheritdoc
118
     */
119 2
    public function getIterator(): Iterator
120
    {
121 2
        foreach ($this->iterator as $key => $value) {
122 2
            $this->preserve_offset ? yield $key => $value : yield $value;
123
        }
124 2
    }
125
126
    /**
127
     * @inheritdoc
128
     */
129 2
    public function count(): int
130
    {
131 2
        return iterator_count($this->iterator);
132
    }
133
134
    /**
135
     * @inheritdoc
136
     */
137 2
    public function jsonSerialize()
138
    {
139 2
        return iterator_to_array($this->convertToUtf8($this->iterator), $this->preserve_offset);
140
    }
141
142
    /**
143
     * Convert Csv file into UTF-8
144
     *
145
     * @param Iterator $iterator
146
     *
147
     * @return Iterator
148
     */
149 8
    protected function convertToUtf8(Iterator $iterator): Iterator
150
    {
151 8
        if (stripos($this->conversion_input_encoding, 'UTF-8') !== false) {
152 6
            return $iterator;
153
        }
154
155
        $convert_cell = function ($value) {
156 2
            return mb_convert_encoding((string) $value, 'UTF-8', $this->conversion_input_encoding);
157 2
        };
158
159
        $convert_row = function (array $row) use ($convert_cell) {
160 2
            $res = [];
161 2
            foreach ($row as $key => $value) {
162 2
                $res[$convert_cell($key)] = $convert_cell($value);
163
            }
164
165 2
            return $res;
166 2
        };
167
168 2
        return new MapIterator($iterator, $convert_row);
169
    }
170
171
    /**
172
     * Returns a HTML table representation of the CSV Table
173
     *
174
     * @param string $class_attr optional classname
175
     *
176
     * @return string
177
     */
178 4
    public function toHTML(string $class_attr = 'table-csv-data', string $offset_attr = 'data-record-offset'): string
179
    {
180 4
        $doc = $this->toXML('table', 'tr', 'td', 'title', $offset_attr);
181 4
        $doc->documentElement->setAttribute('class', $class_attr);
182
183 4
        return $doc->saveHTML($doc->documentElement);
184
    }
185
186
    /**
187
     * Transforms a CSV into a XML
188
     *
189
     * @param string $root_name   XML root node name
190
     * @param string $row_name    XML row node name
191
     * @param string $cell_name   XML cell node name
192
     * @param string $column_attr XML column attribute name
193
     * @param string $offset_attr XML offset attribute name
194
     *
195
     * @return DOMDocument
196
     */
197 6
    public function toXML(
198
        string $root_name = 'csv',
199
        string $row_name = 'row',
200
        string $cell_name = 'cell',
201
        string $column_attr = 'name',
202
        string $offset_attr = 'offset'
203
    ): DOMDocument {
204 6
        $doc = new DOMDocument('1.0', 'UTF-8');
205 6
        $root = $doc->createElement($root_name);
206 6
        foreach ($this->convertToUtf8($this->iterator) as $offset => $row) {
207 6
            $root->appendChild($this->toDOMNode(
208
                $doc,
209
                $row,
210
                $offset,
211
                $row_name,
212
                $cell_name,
213
                $column_attr,
214
                $offset_attr
215
            ));
216
        }
217 6
        $doc->appendChild($root);
218
219 6
        return $doc;
220
    }
221
222
    /**
223
     * convert a Record into a DOMNode
224
     *
225
     * @param DOMDocument $doc         The DOMDocument
226
     * @param array       $row         The CSV record
227
     * @param int         $offset      The CSV record offset
228
     * @param string      $row_name    XML row node name
229
     * @param string      $cell_name   XML cell node name
230
     * @param string      $column_attr XML header attribute name
231
     * @param string      $offset_attr XML offset attribute name
232
     *
233
     * @return DOMElement
234
     */
235 6
    protected function toDOMNode(
236
        DOMDocument $doc,
237
        array $row,
238
        int $offset,
239
        string $row_name,
240
        string $cell_name,
241
        string $column_attr,
242
        string $offset_attr
243
    ): DOMElement {
244 6
        $rowElement = $doc->createElement($row_name);
245 6
        if ($this->preserve_offset) {
246 2
            $rowElement->setAttribute($offset_attr, (string) $offset);
247
        }
248 6
        foreach ($row as $name => $value) {
249 6
            $content = $doc->createTextNode($value);
250 6
            $cell = $doc->createElement($cell_name);
251 6
            if (!empty($this->column_names)) {
252 4
                $cell->setAttribute($column_attr, $name);
253
            }
254 6
            $cell->appendChild($content);
255 6
            $rowElement->appendChild($cell);
256
        }
257
258 6
        return $rowElement;
259
    }
260
261
    /**
262
     * Returns a sequential array of all CSV lines
263
     *
264
     * @return array
265
     */
266 64
    public function fetchAll(): array
267
    {
268 64
        return iterator_to_array($this->iterator, $this->preserve_offset);
269
    }
270
271
    /**
272
     * Returns a single row from the CSV
273
     *
274
     * By default if no offset is provided the first row of the CSV is selected
275
     *
276
     * @param int $offset the CSV row offset
277
     *
278
     * @return array
279
     */
280 6
    public function fetchOne(int $offset = 0): array
281
    {
282 6
        $offset = $this->filterInteger($offset, 0, 'the submitted offset is invalid');
283 4
        $it = new LimitIterator($this->iterator, $offset, 1);
284 4
        $it->rewind();
285
286 4
        return (array) $it->current();
287
    }
288
289
    /**
290
     * Returns the next value from a single CSV column
291
     *
292
     * By default if no column index is provided the first column of the CSV is selected
293
     *
294
     * @param string|int $index CSV column index
295
     *
296
     * @return Generator
297
     */
298 14
    public function fetchColumn($index = 0): Generator
299
    {
300 14
        $offset = $this->getColumnIndex($index, 'the column index value is invalid');
301
        $filter = function (array $row) use ($offset) {
302 12
            return isset($row[$offset]);
303 12
        };
304
305
        $select = function ($row) use ($offset) {
306 10
            return $row[$offset];
307 12
        };
308
309 12
        $iterator = new MapIterator(new CallbackFilterIterator($this->iterator, $filter), $select);
310 12
        foreach ($iterator as $key => $value) {
311 10
            $this->preserve_offset ? yield $key => $value : yield $value;
312
        }
313 8
    }
314
315
    /**
316
     * Filter a column name against the CSV header if any
317
     *
318
     * @param string|int $field         the field name or the field index
319
     * @param string     $error_message the associated error message
320
     *
321
     * @throws InvalidArgumentException if the field is invalid
322
     *
323
     * @return string|int
324
     */
325 22
    protected function getColumnIndex($field, string $error_message)
326
    {
327 22
        if (false !== array_search($field, $this->column_names, true) || is_string($field)) {
328 2
            return $field;
329
        }
330
331 20
        $index = $this->filterInteger($field, 0, $error_message);
332 20
        if (empty($this->column_names)) {
333 16
            return $index;
334
        }
335
336 4
        if (false !== ($index = array_search($index, array_flip($this->column_names), true))) {
337 2
            return $index;
338
        }
339
340 2
        throw new InvalidArgumentException($error_message);
341
    }
342
343
    /**
344
     * Fetches the next key-value pairs from a result set (first
345
     * column is the key, second column is the value).
346
     *
347
     * By default if no column index is provided:
348
     * - the first CSV column is used to provide the keys
349
     * - the second CSV column is used to provide the value
350
     *
351
     * @param string|int $offset_index The column index to serve as offset
352
     * @param string|int $value_index  The column index to serve as value
353
     *
354
     * @return Generator
355
     */
356 8
    public function fetchPairs($offset_index = 0, $value_index = 1): Generator
357
    {
358 8
        $offset = $this->getColumnIndex($offset_index, 'the offset index value is invalid');
359 8
        $value = $this->getColumnIndex($value_index, 'the value index value is invalid');
360
361
        $filter = function ($row) use ($offset) {
362 8
            return isset($row[$offset]);
363 8
        };
364
365 8
        $select = function ($row) use ($offset, $value) {
366 6
            return [$row[$offset], $row[$value] ?? null];
367 8
        };
368
369 8
        $it = new MapIterator(new CallbackFilterIterator($this->iterator, $filter), $select);
370
371 8
        foreach ($it as $row) {
372 6
            yield $row[0] => $row[1];
373
        }
374 8
    }
375
376
    /**
377
     * Sets the CSV encoding charset
378
     *
379
     * @param string $str
380
     *
381
     * @throws InvalidArgumentException if the charset is empty
382
     *
383
     * @return static
384
     */
385 4
    public function setConversionInputEncoding(string $str): self
386
    {
387 4
        $str = str_replace('_', '-', $str);
388 4
        $str = filter_var($str, FILTER_SANITIZE_STRING, ['flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH]);
389 4
        $str = trim($str);
390 4
        if ('' === $str) {
391 2
            throw new InvalidArgumentException('you should use a valid charset');
392
        }
393 2
        $this->conversion_input_encoding = strtoupper($str);
394
395 2
        return $this;
396
    }
397
398
    /**
399
     * Whether we should preserve the CSV document record offset.
400
     *
401
     * If set to true CSV document record offset will added to
402
     * method output where it makes sense.
403
     *
404
     * @param bool $status
405
     *
406
     * @return static
407
     */
408 4
    public function preserveOffset(bool $status)
409
    {
410 4
        $this->preserve_offset = $status;
411
412 4
        return $this;
413
    }
414
}
415