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

RecordSet::toXML()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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