Completed
Pull Request — master (#178)
by ignace nyamagana
02:27
created

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