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

Records::getRow()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 18
rs 9.2
cc 4
eloc 12
nc 3
nop 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 8.1.1
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 DomDocument;
16
use Generator;
17
use InvalidArgumentException;
18
use Iterator;
19
use IteratorAggregate;
20
use JsonSerializable;
21
use League\Csv\Config\Validator;
22
use LimitIterator;
23
use SplFileObject;
24
25
/**
26
 * A class to extract and convert data from a CSV
27
 *
28
 * @package League.csv
29
 * @since  3.0.0
30
 *
31
 */
32
class Records implements IteratorAggregate, JsonSerializable
33
{
34
    use Validator;
35
36
    /**
37
     * @var AbstractCsv
38
     */
39
    protected $csv;
40
41
    /**
42
     * @var Query
43
     */
44
    protected $query;
45
46
    /**
47
     * New Instance
48
     *
49
     * @param AbstractCsv $csv
50
     * @param Query       $query
51
     */
52
    public function __construct(AbstractCsv $csv, Query $query)
53
    {
54
        $this->csv = $csv;
55
        $this->query = $query;
56
    }
57
58
    /**
59
     * @inheritdoc
60
     */
61
    public function getIterator()
62
    {
63
        return $this->query->__invoke($this->csv);
64
    }
65
66
    /**
67
     * Returns a sequential array of all CSV lines
68
     *
69
     * The callable function will be applied to each Iterator item
70
     *
71
     * @return array
72
     */
73
    public function fetchAll()
74
    {
75
        return iterator_to_array($this->getIterator(), false);
76
    }
77
78
    /**
79
     * Returns a single row from the CSV
80
     *
81
     * By default if no offset is provided the first row of the CSV is selected
82
     *
83
     * @param int $offset the CSV row offset
84
     *
85
     * @return array
86
     */
87
    public function fetchOne($offset = 0)
88
    {
89
        $iterator = $this->query->setOffset($offset)->setLimit(1)->__invoke($this->csv);
90
        $iterator->rewind();
91
92
        return (array) $iterator->current();
93
    }
94
95
    /**
96
     * Returns the next value from a single CSV column
97
     *
98
     * The callable function will be applied to each value to be return
99
     *
100
     * By default if no column index is provided the first column of the CSV is selected
101
     *
102
     * @param int $column_index CSV column index
103
     *
104
     * @return Iterator
105
     */
106
    public function fetchColumn($column_index = 0)
107
    {
108
        $column_index = $this->validateInteger($column_index, 0, 'the column index must be a positive integer or 0');
109
110
        $filter_column = function ($row) use ($column_index) {
111
            return isset($row[$column_index]);
112
        };
113
114
        $select_column = function ($row) use ($column_index) {
115
            return $row[$column_index];
116
        };
117
118
        return new MapIterator($this->query->addFilter($filter_column)->__invoke($this->csv), $select_column);
119
    }
120
121
    /**
122
     * Fetches the next key-value pairs from a result set (first
123
     * column is the key, second column is the value).
124
     *
125
     * By default if no column index is provided:
126
     * - the first CSV column is used to provide the keys
127
     * - the second CSV column is used to provide the value
128
     *
129
     * @param int $offset_index The column index to serve as offset
130
     * @param int $value_index  The column index to serve as value
131
     *
132
     * @return Generator
133
     */
134
    public function fetchPairs($offset_index = 0, $value_index = 1)
135
    {
136
        $offset_index = $this->validateInteger($offset_index, 0, 'the offset column index must be a positive integer or 0');
137
        $value_index = $this->validateInteger($value_index, 0, 'the value column index must be a positive integer or 0');
138
        $filter_pairs = function ($row) use ($offset_index) {
139
            return isset($row[$offset_index]);
140
        };
141
        $select_pairs = function ($row) use ($offset_index, $value_index) {
142
            return [
143
                $row[$offset_index],
144
                isset($row[$value_index]) ? $row[$value_index] : null,
145
            ];
146
        };
147
148
        $iterator = $this->query->addFilter($filter_pairs)->__invoke($this->csv);
149
        foreach (new MapIterator($iterator, $select_pairs) as $row) {
150
            yield $row[0] => $row[1];
151
        }
152
    }
153
154
    /**
155
     * Fetch the next row from a result set
156
     *
157
     * The rows are presented as associated arrays
158
     * The callable function will be applied to each row
159
     *
160
     * @param int|array $offset_or_keys the name for each key member OR the row Index to be
161
     *                                  used as the associated named keys
162
     *
163
     * @throws InvalidArgumentException If the submitted keys are invalid
164
     *
165
     * @return Iterator
166
     */
167
    public function fetchAssoc($offset_or_keys = 0)
168
    {
169
        list($iterator, $keys) = $this->getAssocInfos($offset_or_keys);
170
        $keys_count = count($keys);
171
        $combine_array = function (array $row) use ($keys, $keys_count) {
172
            if ($keys_count != count($row)) {
173
                $row = array_slice(array_pad($row, $keys_count, null), 0, $keys_count);
174
            }
175
176
            return array_combine($keys, $row);
177
        };
178
179
        return new MapIterator($iterator, $combine_array);
0 ignored issues
show
Compatibility introduced by
$iterator of type object<Traversable> is not a sub-type of object<Iterator>. It seems like you assume a child interface of the interface Traversable to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
180
    }
181
182
    /**
183
     * Selects the array to be used as key for the fetchAssoc method
184
     *
185
     * @param int|array $offset_or_keys the assoc key OR the row Index to be used
186
     *                                  as the key index
187
     *
188
     * @throws InvalidArgumentException If the row index and/or the resulting array is invalid
189
     *
190
     * @return array
191
     */
192
    protected function getAssocInfos($offset_or_keys)
193
    {
194
        if (is_array($offset_or_keys)) {
195
            return [$this->getIterator(), $this->validateKeys($offset_or_keys)];
196
        }
197
198
        $offset_or_keys = $this->validateInteger(
199
            $offset_or_keys,
200
            0,
201
            'the row index must be a positive integer, 0 or a non empty array'
202
        );
203
        $keys = $this->validateKeys($this->getRow($offset_or_keys));
204
        $filterOutRow = function ($row, $rowIndex) use ($offset_or_keys) {
205
            return $rowIndex != $offset_or_keys;
206
        };
207
208
        return [$this->query->addFilter($filterOutRow)->__invoke($this->csv), $keys];
209
    }
210
211
    /**
212
     * Validates the array to be used by the fetchAssoc method
213
     *
214
     * @param array $keys
215
     *
216
     * @throws InvalidArgumentException If the submitted array fails the assertion
217
     *
218
     * @return array
219
     */
220
    protected function validateKeys(array $keys)
221
    {
222
        if (empty($keys) || $keys !== array_unique(array_filter($keys, [$this, 'isValidKey']))) {
223
            throw new InvalidArgumentException('Use a flat array with unique string values');
224
        }
225
226
        return $keys;
227
    }
228
229
    /**
230
     * Returns whether the submitted value can be used as string
231
     *
232
     * @param mixed $value
233
     *
234
     * @return bool
235
     */
236
    protected function isValidKey($value)
237
    {
238
        return is_scalar($value) || (is_object($value) && method_exists($value, '__toString'));
239
    }
240
241
    /**
242
     * Returns a single row from the CSV without filtering
243
     *
244
     * @param int $offset
245
     *
246
     * @throws InvalidArgumentException If the $offset is not valid or the row does not exist
247
     *
248
     * @return array
249
     */
250
    protected function getRow($offset)
251
    {
252
        $fileObj = $this->csv->getIterator();
253
        $fileObj->setFlags(SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
254
        $iterator = new LimitIterator($fileObj, $offset, 1);
255
        $iterator->rewind();
256
        $line = $iterator->current();
257
        if (empty($line)) {
258
            throw new InvalidArgumentException('the specified row does not exist or is empty');
259
        }
260
261
        $input_bom = $this->csv->getInputBOM();
262
        if (0 === $offset && '' !== $input_bom) {
263
            $line = mb_substr($line, mb_strlen($input_bom));
264
        }
265
266
        return str_getcsv($line, $this->csv->getDelimiter(), $this->csv->getEnclosure(), $this->csv->getEscape());
267
    }
268
269
    /**
270
     * @inheritdoc
271
     */
272
    public function jsonSerialize()
273
    {
274
        return $this->fetchAll();
275
    }
276
277
    /**
278
     * Returns a HTML table representation of the CSV Table
279
     *
280
     * @param string $class_attr optional classname
281
     *
282
     * @return string
283
     */
284
    public function toHTML($class_attr = 'table-csv-data')
285
    {
286
        $doc = $this->toXML('table', 'tr', 'td');
287
        $doc->documentElement->setAttribute('class', $class_attr);
288
289
        return $doc->saveHTML($doc->documentElement);
290
    }
291
292
    /**
293
     * Transforms a CSV into a XML
294
     *
295
     * @param string $root_name XML root node name
296
     * @param string $row_name  XML row node name
297
     * @param string $cell_name XML cell node name
298
     *
299
     * @return DomDocument
300
     */
301
    public function toXML($root_name = 'csv', $row_name = 'row', $cell_name = 'cell')
302
    {
303
        $doc = new DomDocument('1.0', 'UTF-8');
304
        $root = $doc->createElement($root_name);
305
        foreach ($this->getIterator() as $row) {
306
            $rowElement = $doc->createElement($row_name);
307
            array_walk($row, function ($value) use (&$rowElement, $doc, $cell_name) {
308
                $content = $doc->createTextNode($value);
309
                $cell = $doc->createElement($cell_name);
310
                $cell->appendChild($content);
311
                $rowElement->appendChild($cell);
312
            });
313
            $root->appendChild($rowElement);
314
        }
315
        $doc->appendChild($root);
316
317
        return $doc;
318
    }
319
}
320