Completed
Push — master ( d8a5e5...b3757c )
by ignace nyamagana
03:17
created

Reader::validateKeys()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 9.4286
cc 3
eloc 4
nc 2
nop 1
crap 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 8.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 Generator;
16
use InvalidArgumentException;
17
use Iterator;
18
use League\Csv\Modifier\MapIterator;
19
use LimitIterator;
20
use SplFileObject;
21
22
/**
23
 *  A class to manage extracting and filtering a CSV
24
 *
25
 * @package League.csv
26
 * @since  3.0.0
27
 *
28
 */
29
class Reader extends AbstractCsv
30
{
31
    /**
32
     * @inheritdoc
33
     */
34
    protected $stream_filter_mode = STREAM_FILTER_READ;
35
36
    /**
37
     * Return a Filtered Iterator
38
     *
39
     * @param callable|null $callable a callable function to be applied to each Iterator item
40
     *
41
     * @return Iterator
42
     */
43 106
    public function fetch(callable $callable = null)
44
    {
45 106
        return $this->applyCallable($this->getQueryIterator(), $callable);
46
    }
47
48
    /**
49
     * Apply The callable function
50
     *
51
     * @param Iterator      $iterator
52
     * @param callable|null $callable
53
     *
54
     * @return Iterator
55
     */
56 106
    protected function applyCallable(Iterator $iterator, callable $callable = null)
57
    {
58 106
        if (null !== $callable) {
59 50
            return new MapIterator($iterator, $callable);
60
        }
61
62 96
        return $iterator;
63
    }
64
65
    /**
66
     * Returns a sequential array of all CSV lines
67
     *
68
     * The callable function will be applied to each Iterator item
69
     *
70
     * @param callable|null $callable a callable function
71
     *
72
     * @return array
73
     */
74 42
    public function fetchAll(callable $callable = null)
75
    {
76 42
        return $this->applyReturnType(AbstractCsv::TYPE_ARRAY, $this->fetch($callable), false);
77
    }
78
79
    /**
80
     * Applies a callback function on the CSV
81
     *
82
     * The callback function must return TRUE in order to continue
83
     * iterating over the iterator.
84
     *
85
     * @param callable $callable a callable function to apply to each selected CSV rows
86
     *
87
     * @return int the iteration count
88
     */
89 6
    public function each(callable $callable)
90
    {
91 6
        $index = 0;
92 6
        $iterator = $this->fetch();
93 6
        $iterator->rewind();
94 6
        while ($iterator->valid() && true === call_user_func(
95 6
            $callable,
96 6
            $iterator->current(),
97 6
            $iterator->key(),
98
            $iterator
99 6
        )) {
100 6
            ++$index;
101 6
            $iterator->next();
102 6
        }
103
104 6
        return $index;
105
    }
106
107
    /**
108
     * Returns a single row from the CSV
109
     *
110
     * By default if no offset is provided the first row of the CSV is selected
111
     *
112
     * @param int $offset the CSV row offset
113
     *
114
     * @return array
115
     */
116 8
    public function fetchOne($offset = 0)
117
    {
118 8
        $this->setOffset($offset);
119 8
        $this->setLimit(1);
120 8
        $iterator = $this->fetch();
121 8
        $iterator->rewind();
122
123 8
        return (array) $iterator->current();
124
    }
125
126
    /**
127
     * Returns a single column from the CSV data
128
     *
129
     * The callable function will be applied to each value to be return
130
     *
131
     * By default if no column index is provided the first column of the CSV is selected
132
     *
133
     * @param int           $columnIndex CSV column index
134
     * @param callable|null $callable    A callable to be applied to each of the value to be returned.
135
     *
136
     * @return Iterator|array
137
     */
138 14
    public function fetchColumn($columnIndex = 0, callable $callable = null)
139
    {
140 14
        $columnIndex = $this->validateInteger($columnIndex, 0, 'the column index must be a positive integer or 0');
141
142
        $filterColumn = function ($row) use ($columnIndex) {
143 12
            return isset($row[$columnIndex]);
144 12
        };
145
146
        $selectColumn = function ($row) use ($columnIndex) {
147 10
            return $row[$columnIndex];
148 12
        };
149
150 12
        $this->addFilter($filterColumn);
151 12
        $returnType = $this->returnType;
152 12
        $iterator = $this->fetch($selectColumn);
153 12
        $iterator = $this->applyCallable($iterator, $callable);
154
155 12
        return $this->applyReturnType($returnType, $iterator, false);
156
    }
157
158
    /**
159
     * Retrive CSV data as pairs
160
     *
161
     * Fetches an associative array of all rows as key-value pairs (first
162
     * column is the key, second column is the value).
163
     *
164
     * By default if no column index is provided:
165
     * - the first CSV column is used to provide the keys
166
     * - the second CSV column is used to provide the value
167
     *
168
     * @param int           $offsetIndex The column index to serve as offset
169
     * @param int           $valueIndex  The column index to serve as value
170
     * @param callable|null $callable    A callable to be applied to each of the rows to be returned.
171
     *
172
     * @return Generator|array
173
     */
174 18
    public function fetchPairs($offsetIndex = 0, $valueIndex = 1, callable $callable = null)
175
    {
176 18
        $offsetIndex = $this->validateInteger($offsetIndex, 0, 'the offset column index must be a positive integer or 0');
177 18
        $valueIndex = $this->validateInteger($valueIndex, 0, 'the value column index must be a positive integer or 0');
178
        $filterPairs = function ($row) use ($offsetIndex, $valueIndex) {
179 18
            return isset($row[$offsetIndex]);
180 18
        };
181
        $selectPairs = function ($row) use ($offsetIndex, $valueIndex) {
182
            return [
183 16
                $row[$offsetIndex],
184 16
                isset($row[$valueIndex]) ? $row[$valueIndex] : null,
185 16
            ];
186 18
        };
187
188 18
        $this->addFilter($filterPairs);
189 18
        $returnType = $this->returnType;
190 18
        $iterator = $this->fetch($selectPairs);
191 18
        $iterator = $this->applyCallable($iterator, $callable);
192
193 18
        return $this->applyReturnType($returnType, $this->generatePairs($iterator));
194
    }
195
196
    /**
197
     * Return the key/pairs as a PHP generator
198
     *
199
     * @param Iterator $iterator
200
     *
201
     * @return Generator
202
     */
203 18
    protected function generatePairs(Iterator $iterator)
204
    {
205 18
        foreach ($iterator as $row) {
206 16
            yield $row[0] => $row[1];
207 18
        }
208 18
    }
209
210
    /**
211
     * Returns a sequential array of all CSV lines;
212
     *
213
     * The rows are presented as associated arrays
214
     * The callable function will be applied to each Iterator item
215
     *
216
     * @param int|array $offset_or_keys the name for each key member OR the row Index to be
217
     *                                  used as the associated named keys
218
     *
219
     * @param callable $callable A callable to be applied to each of the rows to be returned.
220
     *
221
     * @throws InvalidArgumentException If the submitted keys are invalid
222
     *
223
     * @return Iterator|array
224
     */
225 26
    public function fetchAssoc($offset_or_keys = 0, callable $callable = null)
226
    {
227 26
        $keys = $this->getAssocKeys($offset_or_keys);
228 18
        $keys_count = count($keys);
229
        $combineArray = function (array $row) use ($keys, $keys_count) {
230 18
            if ($keys_count != count($row)) {
231 4
                $row = array_slice(array_pad($row, $keys_count, null), 0, $keys_count);
232 4
            }
233
234 18
            return array_combine($keys, $row);
235 18
        };
236
237 18
        $returnType = $this->returnType;
238 18
        $iterator = $this->fetch($combineArray);
239 18
        $iterator = $this->applyCallable($iterator, $callable);
240
241 18
        return $this->applyReturnType($returnType, $iterator, false);
242
    }
243
244
    /**
245
     * Selects the array to be used as key for the fetchAssoc method
246
     *
247
     * @param int|array $offset_or_keys the assoc key OR the row Index to be used
248
     *                                  as the key index
249
     *
250
     * @throws InvalidArgumentException If the row index and/or the resulting array is invalid
251
     *
252
     * @return array
253
     */
254 26
    protected function getAssocKeys($offset_or_keys)
255
    {
256 26
        if (is_array($offset_or_keys)) {
257 14
            return $this->validateKeys($offset_or_keys);
258
        }
259
260 12
        $offset_or_keys = $this->validateInteger(
261 12
            $offset_or_keys,
262 12
            0,
263
            'the row index must be a positive integer, 0 or a non empty array'
264 12
        );
265 10
        $keys = $this->validateKeys($this->getRow($offset_or_keys));
266 8
        $filterOutRow = function ($row, $rowIndex) use ($offset_or_keys) {
267 8
            return $rowIndex != $offset_or_keys;
268 8
        };
269 8
        $this->addFilter($filterOutRow);
270
271 8
        return $keys;
272
    }
273
274
    /**
275
     * Validates the array to be used by the fetchAssoc method
276
     *
277
     * @param array $keys
278
     *
279
     * @throws InvalidArgumentException If the submitted array fails the assertion
280
     *
281
     * @return array
282
     */
283 22
    protected function validateKeys(array $keys)
284
    {
285 22
        if (empty($keys) || $keys !== array_unique(array_filter($keys, [$this, 'isValidKey']))) {
286 4
            throw new InvalidArgumentException('Use a flat array with unique string values');
287
        }
288
289 18
        return $keys;
290
    }
291
292
    /**
293
     * Returns whether the submitted value can be used as string
294
     *
295
     * @param mixed $value
296
     *
297
     * @return bool
298
     */
299 20
    protected function isValidKey($value)
300
    {
301 20
        return is_scalar($value) || (is_object($value) && method_exists($value, '__toString'));
302
    }
303
304
    /**
305
     * Returns a single row from the CSV without filtering
306
     *
307
     * @param int $offset
308
     *
309
     * @throws InvalidArgumentException If the $offset is not valid or the row does not exist
310
     *
311
     * @return array
312
     */
313 10
    protected function getRow($offset)
314
    {
315 10
        $fileObj = $this->getIterator();
316 10
        $fileObj->setFlags(SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
317 10
        $iterator = new LimitIterator($fileObj, $offset, 1);
318 10
        $iterator->rewind();
319 10
        $line = $iterator->current();
320
321 10
        if (empty($line)) {
322 2
            throw new InvalidArgumentException('the specified row does not exist or is empty');
323
        }
324
325 8
        if (0 === $offset && $this->isBomStrippable()) {
326 4
            $line = mb_substr($line, mb_strlen($this->getInputBom()));
327 4
        }
328
329 8
        return str_getcsv($line, $this->delimiter, $this->enclosure, $this->escape);
330
    }
331
}
332