Completed
Pull Request — master (#178)
by ignace nyamagana
03:42
created

RecordSet::getHeader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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    $iterator
0 ignored issues
show
Bug introduced by
There is no parameter named $iterator. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
57
     * @param Statement $header
0 ignored issues
show
Bug introduced by
There is no parameter named $header. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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