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

RecordSet   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 315
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 27
lcom 1
cbo 3
dl 0
loc 315
ccs 90
cts 90
cp 1
rs 10
c 0
b 0
f 0

16 Methods

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