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

RecordSet::getFieldIndex()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

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