Completed
Pull Request — master (#210)
by ignace nyamagana
02:21
created

RecordSet::__destruct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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