Completed
Pull Request — master (#210)
by ignace nyamagana
03:00
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 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
     * New instance
64
     *
65
     * @param Iterator $iterator     a CSV iterator
66
     * @param array    $column_names the CSV header
67
     */
68 106
    public function __construct(Iterator $iterator, array $column_names = [])
69
    {
70 106
        $this->iterator = $iterator;
71 106
        $this->column_names = $column_names;
72 106
    }
73
74
    /**
75
     * @inheritdoc
76
     */
77 106
    public function __destruct()
78
    {
79 106
        $this->iterator = null;
80 106
    }
81
82
    /**
83
     * Returns the field names associate with the RecordSet
84
     *
85
     * @return string[]
86
     */
87 6
    public function getColumnNames(): array
88
    {
89 6
        return $this->column_names;
90
    }
91
92
    /**
93
     * Returns a specific field names according to its offset
94
     *
95
     * If no field name is found or associated to the submitted
96
     * offset an empty string is returned
97
     *
98
     * @param int $offset
99
     *
100
     * @return string
101
     */
102 4
    public function getColumnName(int $offset): string
103
    {
104 4
        return $this->column_names[$offset] ?? '';
105
    }
106
107
    /**
108
     * @inheritdoc
109
     */
110 2
    public function getIterator(): Iterator
111
    {
112 2
        return $this->iterator;
113
    }
114
115
    /**
116
     * @inheritdoc
117
     */
118 2
    public function count(): int
119
    {
120 2
        return iterator_count($this->iterator);
121
    }
122
123
    /**
124
     * @inheritdoc
125
     */
126 2
    public function jsonSerialize()
127
    {
128 2
        return iterator_to_array($this->convertToUtf8($this->iterator), false);
129
    }
130
131
    /**
132
     * Convert Csv file into UTF-8
133
     *
134
     * @param Iterator $iterator
135
     *
136
     * @return Iterator
137
     */
138 8
    protected function convertToUtf8(Iterator $iterator): Iterator
139
    {
140 8
        if (stripos($this->conversion_input_encoding, 'UTF-8') !== false) {
141 6
            return $iterator;
142
        }
143
144
        $convert_cell = function ($value) {
145 2
            return mb_convert_encoding((string) $value, 'UTF-8', $this->conversion_input_encoding);
146 2
        };
147
148
        $convert_row = function (array $row) use ($convert_cell) {
149 2
            $res = [];
150 2
            foreach ($row as $key => $value) {
151 2
                $res[$convert_cell($key)] = $convert_cell($value);
152
            }
153
154 2
            return $res;
155 2
        };
156
157 2
        return new MapIterator($iterator, $convert_row);
158
    }
159
160
    /**
161
     * Returns a HTML table representation of the CSV Table
162
     *
163
     * @param string $class_attr optional classname
164
     *
165
     * @return string
166
     */
167 4
    public function toHTML(string $class_attr = 'table-csv-data'): string
168
    {
169 4
        $doc = $this->toXML('table', 'tr', 'td', 'title');
170 4
        $doc->documentElement->setAttribute('class', $class_attr);
171
172 4
        return $doc->saveHTML($doc->documentElement);
173
    }
174
175
    /**
176
     * Transforms a CSV into a XML
177
     *
178
     * @param string $root_name        XML root node name
179
     * @param string $row_name         XML row node name
180
     * @param string $cell_name        XML cell node name
181
     * @param string $column_attr_name XML column attribute name
182
     *
183
     * @return DOMDocument
184
     */
185 6
    public function toXML(
186
        string $root_name = 'csv',
187
        string $row_name = 'row',
188
        string $cell_name = 'cell',
189
        string $column_attr_name = 'name'
190
    ): DOMDocument {
191 6
        $doc = new DOMDocument('1.0', 'UTF-8');
192 6
        $root = $doc->createElement($root_name);
193 6
        foreach ($this->convertToUtf8($this->iterator) as $row) {
194 6
            $root->appendChild($this->toDOMNode($doc, $row, $row_name, $cell_name, $column_attr_name));
195
        }
196 6
        $doc->appendChild($root);
197
198 6
        return $doc;
199
    }
200
201
    /**
202
     * convert a Record into a DOMNode
203
     *
204
     * @param DOMDocument $doc              The DOMDocument
205
     * @param array       $row              The CSV record
206
     * @param string      $row_name         XML row node name
207
     * @param string      $cell_name        XML cell node name
208
     * @param string      $column_attr_name XML header attribute name
209
     *
210
     * @return DOMElement
211
     */
212 6
    protected function toDOMNode(
213
        DOMDocument $doc,
214
        array $row,
215
        string $row_name,
216
        string $cell_name,
217
        string $column_attr_name
218
    ): DOMElement {
219 6
        $rowElement = $doc->createElement($row_name);
220 6
        foreach ($row as $name => $value) {
221 6
            $content = $doc->createTextNode($value);
222 6
            $cell = $doc->createElement($cell_name);
223 6
            if (!empty($this->column_names)) {
224 4
                $cell->setAttribute($column_attr_name, $name);
225
            }
226 6
            $cell->appendChild($content);
227 6
            $rowElement->appendChild($cell);
228
        }
229
230 6
        return $rowElement;
231
    }
232
233
    /**
234
     * Returns a sequential array of all CSV lines
235
     *
236
     * @return array
237
     */
238 62
    public function fetchAll(): array
239
    {
240 62
        return iterator_to_array($this->iterator);
241
    }
242
243
    /**
244
     * Returns a single row from the CSV
245
     *
246
     * By default if no offset is provided the first row of the CSV is selected
247
     *
248
     * @param int $offset the CSV row offset
249
     *
250
     * @return array
251
     */
252 6
    public function fetchOne(int $offset = 0): array
253
    {
254 6
        $offset = $this->filterInteger($offset, 0, 'the submitted offset is invalid');
255 4
        $it = new LimitIterator($this->iterator, $offset, 1);
256 4
        $it->rewind();
257
258 4
        return (array) $it->current();
259
    }
260
261
    /**
262
     * Returns the next value from a single CSV column
263
     *
264
     * By default if no column index is provided the first column of the CSV is selected
265
     *
266
     * @param string|int $index CSV column index
267
     *
268
     * @return Iterator
269
     */
270 14
    public function fetchColumn($index = 0): Iterator
271
    {
272 14
        $offset = $this->getColumnIndex($index, 'the column index value is invalid');
273
        $filter = function (array $row) use ($offset) {
274 12
            return isset($row[$offset]);
275 12
        };
276
277
        $select = function ($row) use ($offset) {
278 8
            return $row[$offset];
279 12
        };
280
281 12
        return new MapIterator(new CallbackFilterIterator($this->iterator, $filter), $select);
282
    }
283
284
    /**
285
     * Filter a column name against the CSV header if any
286
     *
287
     * @param string|int $field         the field name or the field index
288
     * @param string     $error_message the associated error message
289
     *
290
     * @throws Exception if the field is invalid
291
     *
292
     * @return string|int
293
     */
294 22
    protected function getColumnIndex($field, string $error_message)
295
    {
296 22
        if (false !== array_search($field, $this->column_names, true) || is_string($field)) {
297 2
            return $field;
298
        }
299
300 20
        $index = $this->filterInteger($field, 0, $error_message);
301 20
        if (empty($this->column_names)) {
302 16
            return $index;
303
        }
304
305 4
        if (false !== ($index = array_search($index, array_flip($this->column_names), true))) {
306 2
            return $index;
307
        }
308
309 2
        throw new Exception($error_message);
310
    }
311
312
    /**
313
     * Fetches the next key-value pairs from a result set (first
314
     * column is the key, second column is the value).
315
     *
316
     * By default if no column index is provided:
317
     * - the first CSV column is used to provide the keys
318
     * - the second CSV column is used to provide the value
319
     *
320
     * @param string|int $offset_index The column index to serve as offset
321
     * @param string|int $value_index  The column index to serve as value
322
     *
323
     * @return Generator
324
     */
325 8
    public function fetchPairs($offset_index = 0, $value_index = 1): Generator
326
    {
327 8
        $offset = $this->getColumnIndex($offset_index, 'the offset index value is invalid');
328 8
        $value = $this->getColumnIndex($value_index, 'the value index value is invalid');
329
330
        $filter = function ($row) use ($offset) {
331 8
            return isset($row[$offset]);
332 8
        };
333
334 8
        $select = function ($row) use ($offset, $value) {
335 6
            return [$row[$offset], $row[$value] ?? null];
336 8
        };
337
338 8
        $it = new MapIterator(new CallbackFilterIterator($this->iterator, $filter), $select);
339
340 8
        foreach ($it as $row) {
341 6
            yield $row[0] => $row[1];
342
        }
343 8
    }
344
345
    /**
346
     * Sets the CSV encoding charset
347
     *
348
     * @param string $str
349
     *
350
     * @return static
351
     */
352 4
    public function setConversionInputEncoding(string $str): self
353
    {
354 4
        $str = str_replace('_', '-', $str);
355 4
        $str = filter_var($str, FILTER_SANITIZE_STRING, ['flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH]);
356 4
        $str = trim($str);
357 4
        if ('' === $str) {
358 2
            throw new Exception('you should use a valid charset');
359
        }
360 2
        $this->conversion_input_encoding = strtoupper($str);
361
362 2
        return $this;
363
    }
364
}
365