Completed
Push — master ( 9e791e...f99e5a )
by ignace nyamagana
26:13 queued 24:20
created

RecordSet::count()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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