Completed
Pull Request — master (#178)
by ignace nyamagana
05:35
created

RecordSet::fetchPairs()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 10
nc 2
nop 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
namespace League\Csv;
14
15
use ArrayIterator;
16
use CallbackFilterIterator;
17
use Countable;
18
use DOMDocument;
19
use Generator;
20
use InvalidArgumentException;
21
use Iterator;
22
use IteratorAggregate;
23
use JsonSerializable;
24
use League\Csv\Config\Validator;
25
use LimitIterator;
26
27
/**
28
 * A class to extract and convert data from a CSV
29
 *
30
 * @package League.csv
31
 * @since  9.0.0
32
 *
33
 */
34
class RecordSet implements Countable, IteratorAggregate, JsonSerializable
35
{
36
    use Validator;
37
38
    /**
39
     * @var array
40
     */
41
    protected $header;
42
43
    /**
44
     * @var array
45
     */
46
    protected $flip_header;
47
48
    /**
49
     * @var Iterator
50
     */
51
    protected $iterator;
52
53
    /**
54
     * New Instance
55
     *
56
     * @param Reader    $csv
57
     * @param Statement $stmt
58
     */
59
    public function __construct(Reader $csv, Statement $stmt)
60
    {
61
        $filters = $stmt->getFilter();
62
        if (null !== ($header_offset = $csv->getHeaderOffset())) {
63
            array_unshift($filters, function (array $record, $index) use ($header_offset) {
64
                return $index !== $header_offset;
65
            });
66
        }
67
68
        $iterator = $this->prepare($csv);
69
        $iterator = $this->filter($iterator, $filters);
70
        $iterator = $this->sort($iterator, $stmt->getSortBy());
71
72
        $this->iterator = new LimitIterator($iterator, $stmt->getOffset(), $stmt->getLimit());
73
    }
74
75
    /**
76
     * Prepare the Reader for manipulation
77
     *
78
     * - remove the BOM sequence if present
79
     * - attach the header to the records if present
80
     * - convert the CSV to UTF-8 if needed
81
     *
82
     * @param Reader $csv
83
     *
84
     * @throws InvalidRowException if the column is inconsistent
85
     *
86
     * @return Iterator
87
     */
88
    protected function prepare(Reader $csv)
89
    {
90
        $this->header = $csv->getHeader();
91
        $this->flip_header = array_flip($this->header);
92
        $input_encoding = $csv->getInputEncoding();
93
        $use_converter = $this->useInternalConverter($csv);
94
        $iterator = $this->removeBOM($csv);
95
        if (!empty($this->header)) {
96
            $header_column_count = count($this->header);
97
            $combine_array = function (array $record) use ($header_column_count) {
98
                if ($header_column_count != count($record)) {
99
                    throw new InvalidRowException('csv_consistency', $record, 'record and header column count differ');
100
                }
101
102
                return array_combine($this->header, $record);
103
            };
104
            $iterator = new MapIterator($iterator, $combine_array);
105
        }
106
107
        return $this->convert($iterator, $input_encoding, $use_converter);
108
    }
109
110
    /**
111
     * Remove the BOM sequence from the CSV
112
     *
113
     * @param Reader $csv
114
     *
115
     * @return Iterator
116
     */
117
    protected function removeBOM(Reader $csv)
118
    {
119
        $bom = $csv->getInputBOM();
120
        if ('' === $bom) {
121
            return $csv->getIterator();
122
        }
123
124
        $enclosure = $csv->getEnclosure();
125
        $formatter = function (array $record, $index) use ($bom, $enclosure) {
126
            if (0 != $index) {
127
                return $record;
128
            }
129
130
            return $this->stripBOM($record, $bom, $enclosure);
131
        };
132
133
        return new MapIterator($csv->getIterator(), $formatter);
134
    }
135
136
    /**
137
     * Convert the iterator to UTF-8 if needed
138
     *
139
     * @param Iterator $iterator
140
     * @param string   $input_encoding
141
     * @param bool     $use_converter
142
     *
143
     * @return Iterator
144
     */
145
    protected function convert(Iterator $iterator, $input_encoding, $use_converter)
146
    {
147
        if (!$use_converter) {
148
            return $iterator;
149
        }
150
151
        $converter = function ($record) use ($input_encoding) {
152
            return $this->convertRecordToUtf8($record, $input_encoding);
153
        };
154
155
        return new MapIterator($iterator, $converter);
156
    }
157
158
    /**
159
    * Filter the Iterator
160
    *
161
    * @param Iterator $iterator
162
    * @param callable[] $filters
163
    *
164
    * @return Iterator
165
    */
166
    protected function filter(Iterator $iterator, array $filters)
167
    {
168
        $reducer = function ($iterator, $callable) {
169
            return new CallbackFilterIterator($iterator, $callable);
170
        };
171
172
        array_unshift($filters, function ($row) {
173
            return is_array($row) && $row != [null];
174
        });
175
176
        return array_reduce($filters, $reducer, $iterator);
177
    }
178
179
    /**
180
    * Sort the Iterator
181
    *
182
    * @param Iterator $iterator
183
    * @param callable[] $sort
184
    *
185
    * @return Iterator
186
    */
187
    protected function sort(Iterator $iterator, array $sort)
188
    {
189
        if (empty($sort)) {
190
            return $iterator;
191
        }
192
193
        $obj = new ArrayIterator(iterator_to_array($iterator));
194
        $obj->uasort(function ($record_a, $record_b) use ($sort) {
195
            $res = 0;
196
            foreach ($sort as $compare) {
197
                if (0 !== ($res = call_user_func($compare, $record_a, $record_b))) {
198
                    break;
199
                }
200
            }
201
202
            return $res;
203
        });
204
205
        return $obj;
206
    }
207
208
    /**
209
     * @inheritdoc
210
     */
211
    public function __destruct()
212
    {
213
        $this->iterator = null;
214
    }
215
216
    /**
217
     * @inheritdoc
218
     */
219
    public function getIterator()
220
    {
221
        return $this->iterator;
222
    }
223
224
    /**
225
     * Returns the object header
226
     *
227
     * @return string[]
228
     */
229
    public function getHeader()
230
    {
231
        return $this->header;
232
    }
233
234
    /**
235
     * Returns a HTML table representation of the CSV Table
236
     *
237
     * @param string $class_attr optional classname
238
     *
239
     * @return string
240
     */
241
    public function toHTML($class_attr = 'table-csv-data')
242
    {
243
        $doc = $this->toXML('table', 'tr', 'td');
244
        $doc->documentElement->setAttribute('class', $class_attr);
245
246
        return $doc->saveHTML($doc->documentElement);
247
    }
248
249
    /**
250
     * Transforms a CSV into a XML
251
     *
252
     * @param string $root_name XML root node name
253
     * @param string $row_name  XML row node name
254
     * @param string $cell_name XML cell node name
255
     *
256
     * @return DOMDocument
257
     */
258
    public function toXML($root_name = 'csv', $row_name = 'row', $cell_name = 'cell')
259
    {
260
        $this->row_name = $this->validateString($row_name);
261
        $this->cell_name = $this->validateString($cell_name);
262
        $doc = new DOMDocument('1.0', 'UTF-8');
263
        $root = $doc->createElement($this->validateString($root_name));
264
        if (!empty($this->header)) {
265
            $root->appendChild($this->convertRecordToDOMNode($this->header, $doc));
266
        }
267
268
        foreach ($this->iterator as $row) {
269
            $root->appendChild($this->convertRecordToDOMNode($row, $doc));
270
        }
271
        $doc->appendChild($root);
272
273
        return $doc;
274
    }
275
276
    /**
277
     * @inheritdoc
278
     */
279
    public function count()
280
    {
281
        return iterator_count($this->iterator);
282
    }
283
284
    /**
285
     * @inheritdoc
286
     */
287
    public function jsonSerialize()
288
    {
289
        return $this->fetchAll();
290
    }
291
292
    /**
293
     * Returns a sequential array of all founded RecordSet
294
     *
295
     * @return array
296
     */
297
    public function fetchAll()
298
    {
299
        return iterator_to_array($this->iterator, false);
300
    }
301
302
    /**
303
     * Returns a single record from the recordSet
304
     *
305
     * @param int $offset the record offset relative to the RecordSet
306
     *
307
     * @return array
308
     */
309
    public function fetchOne($offset = 0)
310
    {
311
        $offset = $this->validateInteger($offset, 0, 'the submitted offset is invalid');
312
        $it = new LimitIterator($this->iterator, $offset, 1);
313
        $it->rewind();
314
315
        return (array) $it->current();
316
    }
317
318
    /**
319
     * Returns the next value from a specific record column
320
     *
321
     * By default if no column index is provided the first column of the founded RecordSet is returned
322
     *
323
     * @param string|int $column_index CSV column index or header field name
324
     *
325
     * @return Iterator
326
     */
327
    public function fetchColumn($column_index = 0)
328
    {
329
        $column_index = $this->filterFieldName($column_index, 'the column index value is invalid');
330
        $filter = function (array $record) use ($column_index) {
331
            return isset($record[$column_index]);
332
        };
333
        $select = function (array $record) use ($column_index) {
334
            return $record[$column_index];
335
        };
336
337
        return new MapIterator(new CallbackFilterIterator($this->iterator, $filter), $select);
338
    }
339
340
    /**
341
     * Filter a field name against the CSV header if any
342
     *
343
     * @param string|int $field         the field name or the field index
344
     * @param string     $error_message the associated error message
345
     *
346
     * @throws InvalidArgumentException if the field is invalid
347
     *
348
     * @return string|int
349
     */
350
    protected function filterFieldName($field, $error_message)
351
    {
352
        if (false !== array_search($field, $this->header, true)) {
353
            return $field;
354
        }
355
356
        $index = $this->validateInteger($field, 0, $error_message);
357
        if (empty($this->header)) {
358
            return $index;
359
        }
360
361
        if (false !== ($index = array_search($index, $this->flip_header, true))) {
362
            return $index;
363
        }
364
365
        throw new InvalidArgumentException($error_message);
366
    }
367
368
    /**
369
     * Fetches the next key-value pairs from a result set (first
370
     * column is the key, second column is the value).
371
     *
372
     * By default if no column index is provided:
373
     * - the first CSV column is used to provide the keys
374
     * - the second CSV column is used to provide the value
375
     *
376
     * @param string|int $offset_index The field index or name to serve as offset
377
     * @param string|int $value_index  The field index or name to serve as value
378
     *
379
     * @return Generator
380
     */
381
    public function fetchPairs($offset_index = 0, $value_index = 1)
382
    {
383
        $offset_index = $this->filterFieldName($offset_index, 'the offset column index is invalid');
384
        $value_index = $this->filterFieldName($value_index, 'the value column index is invalid');
385
        $filter = function (array $record) use ($offset_index) {
386
            return isset($record[$offset_index]);
387
        };
388
        $select = function (array $record) use ($offset_index, $value_index) {
389
            return [$record[$offset_index], isset($record[$value_index]) ? $record[$value_index] : null];
390
        };
391
392
        $iterator = new MapIterator(new CallbackFilterIterator($this->iterator, $filter), $select);
393
        foreach ($iterator as $row) {
394
            yield $row[0] => $row[1];
395
        }
396
    }
397
}
398