Completed
Pull Request — master (#178)
by ignace nyamagana
22:48 queued 14:18
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 Countable;
16
use DomDocument;
17
use Generator;
18
use InvalidArgumentException;
19
use Iterator;
20
use IteratorAggregate;
21
use JsonSerializable;
22
use League\Csv\Config\Validator;
23
use LimitIterator;
24
use SplFileObject;
25
26
/**
27
 * A class to extract and convert data from a CSV
28
 *
29
 * @package League.csv
30
 * @since  3.0.0
31
 *
32
 */
33
class Records implements Countable, IteratorAggregate, JsonSerializable
34
{
35
    use Validator;
36
37
    /**
38
     * @var AbstractCsv
39
     */
40
    protected $csv;
41
42
    /**
43
     * @var Query
44
     */
45
    protected $query;
46
47
    /**
48
     * New Instance
49
     *
50
     * @param AbstractCsv $csv
51
     * @param Query       $query
52
     */
53
    public function __construct(AbstractCsv $csv, Query $query)
54
    {
55
        $this->csv = $csv;
56
        $this->query = $query;
57
    }
58
59
    /**
60
     * @inheritdoc
61
     */
62
    public function getIterator()
63
    {
64
        return $this->query->__invoke($this->csv);
65
    }
66
67
    /**
68
     * @inheritdoc
69
     */
70
    public function count()
71
    {
72
        return iterator_count($this);
73
    }
74
75
    /**
76
     * Returns a sequential array of all CSV lines
77
     *
78
     * The callable function will be applied to each Iterator item
79
     *
80
     * @return array
81
     */
82
    public function fetchAll()
83
    {
84
        return iterator_to_array($this, false);
85
    }
86
87
    /**
88
     * Returns a single row from the CSV
89
     *
90
     * By default if no offset is provided the first row of the CSV is selected
91
     *
92
     * @param int $offset the CSV row offset
93
     *
94
     * @return array
95
     */
96
    public function fetchOne($offset = 0)
97
    {
98
        $iterator = $this->query->setOffset($offset)->setLimit(1)->__invoke($this->csv);
99
        $iterator->rewind();
100
101
        return (array) $iterator->current();
102
    }
103
104
    /**
105
     * Returns the next value from a single CSV column
106
     *
107
     * The callable function will be applied to each value to be return
108
     *
109
     * By default if no column index is provided the first column of the CSV is selected
110
     *
111
     * @param int $column_index CSV column index
112
     *
113
     * @return Iterator
114
     */
115
    public function fetchColumn($column_index = 0)
116
    {
117
        $column_index = $this->validateInteger($column_index, 0, 'the column index must be a positive integer or 0');
118
119
        $filter_column = function ($row) use ($column_index) {
120
            return isset($row[$column_index]);
121
        };
122
123
        $select_column = function ($row) use ($column_index) {
124
            return $row[$column_index];
125
        };
126
127
        return new MapIterator($this->query->addFilter($filter_column)->__invoke($this->csv), $select_column);
128
    }
129
130
    /**
131
     * Fetches the next key-value pairs from a result set (first
132
     * column is the key, second column is the value).
133
     *
134
     * By default if no column index is provided:
135
     * - the first CSV column is used to provide the keys
136
     * - the second CSV column is used to provide the value
137
     *
138
     * @param int $offset_index The column index to serve as offset
139
     * @param int $value_index  The column index to serve as value
140
     *
141
     * @return Generator
142
     */
143
    public function fetchPairs($offset_index = 0, $value_index = 1)
144
    {
145
        $offset_index = $this->validateInteger($offset_index, 0, 'the offset column index must be a positive integer or 0');
146
        $value_index = $this->validateInteger($value_index, 0, 'the value column index must be a positive integer or 0');
147
        $filter_pairs = function ($row) use ($offset_index) {
148
            return isset($row[$offset_index]);
149
        };
150
        $select_pairs = function ($row) use ($offset_index, $value_index) {
151
            return [
152
                $row[$offset_index],
153
                isset($row[$value_index]) ? $row[$value_index] : null,
154
            ];
155
        };
156
157
        $iterator = $this->query->addFilter($filter_pairs)->__invoke($this->csv);
158
        foreach (new MapIterator($iterator, $select_pairs) as $row) {
159
            yield $row[0] => $row[1];
160
        }
161
    }
162
163
    /**
164
     * Fetch the next row from a result set
165
     *
166
     * The rows are presented as associated arrays
167
     * The callable function will be applied to each row
168
     *
169
     * @param int|array $offset_or_keys the name for each key member OR the row Index to be
170
     *                                  used as the associated named keys
171
     *
172
     * @throws InvalidArgumentException If the submitted keys are invalid
173
     *
174
     * @return Iterator
175
     */
176
    public function fetchAssoc($offset_or_keys = 0)
177
    {
178
        list($iterator, $keys) = $this->getAssocInfos($offset_or_keys);
179
        $keys_count = count($keys);
180
        $combine_array = function (array $row) use ($keys, $keys_count) {
181
            if ($keys_count != count($row)) {
182
                $row = array_slice(array_pad($row, $keys_count, null), 0, $keys_count);
183
            }
184
185
            return array_combine($keys, $row);
186
        };
187
188
        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...
189
    }
190
191
    /**
192
     * Selects the array to be used as key for the fetchAssoc method
193
     *
194
     * @param int|array $offset_or_keys the assoc key OR the row Index to be used
195
     *                                  as the key index
196
     *
197
     * @throws InvalidArgumentException If the row index and/or the resulting array is invalid
198
     *
199
     * @return array
200
     */
201
    protected function getAssocInfos($offset_or_keys)
202
    {
203
        if (is_array($offset_or_keys)) {
204
            return [$this->getIterator(), $this->validateKeys($offset_or_keys)];
205
        }
206
207
        $offset_or_keys = $this->validateInteger(
208
            $offset_or_keys,
209
            0,
210
            'the row index must be a positive integer, 0 or a non empty array'
211
        );
212
        $keys = $this->validateKeys($this->getRow($offset_or_keys));
213
        $filterOutRow = function ($row, $rowIndex) use ($offset_or_keys) {
214
            return $rowIndex != $offset_or_keys;
215
        };
216
217
        return [$this->query->addFilter($filterOutRow)->__invoke($this->csv), $keys];
218
    }
219
220
    /**
221
     * Validates the array to be used by the fetchAssoc method
222
     *
223
     * @param array $keys
224
     *
225
     * @throws InvalidArgumentException If the submitted array fails the assertion
226
     *
227
     * @return array
228
     */
229
    protected function validateKeys(array $keys)
230
    {
231
        if (empty($keys) || $keys !== array_unique(array_filter($keys, [$this, 'isValidKey']))) {
232
            throw new InvalidArgumentException('Use a flat array with unique string values');
233
        }
234
235
        return $keys;
236
    }
237
238
    /**
239
     * Returns whether the submitted value can be used as string
240
     *
241
     * @param mixed $value
242
     *
243
     * @return bool
244
     */
245
    protected function isValidKey($value)
246
    {
247
        return is_scalar($value) || (is_object($value) && method_exists($value, '__toString'));
248
    }
249
250
    /**
251
     * Returns a single row from the CSV without filtering
252
     *
253
     * @param int $offset
254
     *
255
     * @throws InvalidArgumentException If the $offset is not valid or the row does not exist
256
     *
257
     * @return array
258
     */
259
    protected function getRow($offset)
260
    {
261
        $fileObj = $this->csv->getIterator();
262
        $fileObj->setFlags(SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
263
        $iterator = new LimitIterator($fileObj, $offset, 1);
264
        $iterator->rewind();
265
        $line = $iterator->current();
266
        if (empty($line)) {
267
            throw new InvalidArgumentException('the specified row does not exist or is empty');
268
        }
269
270
        $input_bom = $this->csv->getInputBOM();
271
        if (0 === $offset && '' !== $input_bom) {
272
            $line = mb_substr($line, mb_strlen($input_bom));
273
        }
274
275
        return str_getcsv($line, $this->csv->getDelimiter(), $this->csv->getEnclosure(), $this->csv->getEscape());
276
    }
277
278
    /**
279
     * @inheritdoc
280
     */
281
    public function jsonSerialize()
282
    {
283
        return $this->fetchAll();
284
    }
285
286
    /**
287
     * Returns a HTML table representation of the CSV Table
288
     *
289
     * @param string $class_attr optional classname
290
     *
291
     * @return string
292
     */
293
    public function toHTML($class_attr = 'table-csv-data')
294
    {
295
        $doc = $this->toXML('table', 'tr', 'td');
296
        $doc->documentElement->setAttribute('class', $class_attr);
297
298
        return $doc->saveHTML($doc->documentElement);
299
    }
300
301
    /**
302
     * Transforms a CSV into a XML
303
     *
304
     * @param string $root_name XML root node name
305
     * @param string $row_name  XML row node name
306
     * @param string $cell_name XML cell node name
307
     *
308
     * @return DomDocument
309
     */
310
    public function toXML($root_name = 'csv', $row_name = 'row', $cell_name = 'cell')
311
    {
312
        $doc = new DomDocument('1.0', 'UTF-8');
313
        $root = $doc->createElement($root_name);
314
        foreach ($this->getIterator() as $row) {
315
            $rowElement = $doc->createElement($row_name);
316
            array_walk($row, function ($value) use (&$rowElement, $doc, $cell_name) {
317
                $content = $doc->createTextNode($value);
318
                $cell = $doc->createElement($cell_name);
319
                $cell->appendChild($content);
320
                $rowElement->appendChild($cell);
321
            });
322
            $root->appendChild($rowElement);
323
        }
324
        $doc->appendChild($root);
325
326
        return $doc;
327
    }
328
}
329