Completed
Push — master ( dcbe24...225712 )
by ignace nyamagana
31:38 queued 20:47
created

Reader::applyCallable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

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 2
eloc 4
nc 2
nop 2
crap 2
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 104
    public function fetch(callable $callable = null)
44
    {
45
        $this->addFilter(function ($row) {
46 94
            return is_array($row);
47 104
        });
48 104
        $iterator = $this->getIterator();
49 104
        $iterator = $this->applyBomStripping($iterator);
50 104
        $iterator = $this->applyIteratorFilter($iterator);
51 104
        $iterator = $this->applyIteratorSortBy($iterator);
52 104
        $iterator = $this->applyIteratorInterval($iterator);
53 104
        $this->returnType = self::TYPE_ARRAY;
54
55 104
        return $this->applyCallable($iterator, $callable);
56
    }
57
58
    /**
59
     * Apply The callable function
60
     *
61
     * @param Iterator      $iterator
62
     * @param callable|null $callable
63
     *
64
     * @return Iterator
65
     */
66 104
    protected function applyCallable(Iterator $iterator, callable $callable = null)
67
    {
68 104
        if (null !== $callable) {
69 48
            return new MapIterator($iterator, $callable);
70
        }
71
72 94
        return $iterator;
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
     * @param callable|null $callable a callable function
81
     *
82
     * @return array
83
     */
84 42
    public function fetchAll(callable $callable = null)
85
    {
86 42
        return $this->applyReturnType(self::TYPE_ARRAY, $this->fetch($callable), false);
87
    }
88
89
    /**
90
     * Convert the Iterator into an array depending on the selected return type
91
     *
92
     * @param int      $type
93
     * @param Iterator $iterator
94
     * @param bool     $use_keys Whether to use the iterator element keys as index
95
     *
96
     * @return Iterator|array
97
     */
98 88
    protected function applyReturnType($type, Iterator $iterator, $use_keys = true)
99
    {
100 88
        if (self::TYPE_ARRAY == $type) {
101 72
            return iterator_to_array($iterator, $use_keys);
102
        }
103
104 16
        return $iterator;
105
    }
106
107
    /**
108
     * Applies a callback function on the CSV
109
     *
110
     * The callback function must return TRUE in order to continue
111
     * iterating over the iterator.
112
     *
113
     * @param callable $callable a callable function to apply to each selected CSV rows
114
     *
115
     * @return int the iteration count
116
     */
117 6
    public function each(callable $callable)
118
    {
119 6
        $index = 0;
120 6
        $iterator = $this->fetch();
121 6
        $iterator->rewind();
122 6
        while ($iterator->valid() && true === call_user_func(
123 6
            $callable,
124 6
            $iterator->current(),
125 6
            $iterator->key(),
126
            $iterator
127 6
        )) {
128 6
            ++$index;
129 6
            $iterator->next();
130 6
        }
131
132 6
        return $index;
133
    }
134
135
    /**
136
     * Returns a single row from the CSV
137
     *
138
     * By default if no offset is provided the first row of the CSV is selected
139
     *
140
     * @param int $offset the CSV row offset
141
     *
142
     * @return array
143
     */
144 8
    public function fetchOne($offset = 0)
145
    {
146 8
        $this->setOffset($offset);
147 8
        $this->setLimit(1);
148 8
        $iterator = $this->fetch();
149 8
        $iterator->rewind();
150
151 8
        return (array) $iterator->current();
152
    }
153
154
    /**
155
     * Returns a single column from the CSV data
156
     *
157
     * The callable function will be applied to each value to be return
158
     *
159
     * By default if no column index is provided the first column of the CSV is selected
160
     *
161
     * @param int           $columnIndex field Index
162
     * @param callable|null $callable    a callable function to apply to each selected CSV rows column value.
163
     *                                   The callable takes up to three parameter
164
     *                                   - the column value
165
     *                                   - the row index
166
     *                                   - the CSV Iterator
167
     *
168
     * @return Iterator|array
169
     */
170 16
    public function fetchColumn($columnIndex = 0, callable $callable = null)
171
    {
172 16
        $this->assertValidColumnIndex($columnIndex);
173
174
        $filterColumn = function ($row) use ($columnIndex) {
175 12
            return array_key_exists($columnIndex, $row);
176 14
        };
177
178
        $selectColumn = function ($row) use ($columnIndex) {
179 10
            return $row[$columnIndex];
180 14
        };
181
182 14
        $this->addFilter($filterColumn);
183 14
        $type = $this->returnType;
184 14
        $iterator = $this->fetch($selectColumn);
185 14
        $iterator = $this->applyCallable($iterator, $callable);
186
187 14
        return $this->applyReturnType($type, $iterator, false);
188
    }
189
190
    /**
191
     * Validate a CSV row index
192
     *
193
     * @param int $index
194
     *
195
     * @throws InvalidArgumentException If the column index is not a positive integer or 0
196
     */
197 30
    protected function assertValidColumnIndex($index)
198
    {
199 30 View Code Duplication
        if (false === filter_var($index, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) {
200 2
            throw new InvalidArgumentException('the column index must be a positive integer or 0');
201
        }
202 28
    }
203
204
    /**
205
     * Retrive CSV data as pairs
206
     *
207
     * Fetches an associative array of all rows as key-value pairs (first
208
     * column is the key, second column is the value).
209
     *
210
     * By default if no column index is provided:
211
     * - the first CSV column is used to provide the keys
212
     * - the second CSV column is used to provide the value
213
     *
214
     * @param int           $offsetColumnIndex The column index to server as offset
215
     * @param int           $valueColumnIndex  The column index to server as value
216
     * @param callable|null $callable          A callable to be applied to each of the rows to be returned.
217
     *
218
     * @return Generator|array
219
     */
220 14
    public function fetchPairs($offsetColumnIndex = 0, $valueColumnIndex = 1, callable $callable = null)
221
    {
222 14
        $this->assertValidColumnIndex($offsetColumnIndex);
223 14
        $this->assertValidColumnIndex($valueColumnIndex);
224
        $filterPairs = function ($row) use ($offsetColumnIndex, $valueColumnIndex) {
225 12
            return array_key_exists($offsetColumnIndex, $row) && array_key_exists($valueColumnIndex, $row);
226 14
        };
227
        $selectPairs = function ($row) use ($offsetColumnIndex, $valueColumnIndex) {
228 12
            return [$row[$offsetColumnIndex], $row[$valueColumnIndex]];
229 14
        };
230 14
        $this->addFilter($filterPairs);
231 14
        $type = $this->returnType;
232 14
        $iterator = $this->fetch($selectPairs);
233 14
        $iterator = $this->applyCallable($iterator, $callable);
234
235 14
        return $this->applyReturnType($type, $this->fetchPairsGenerator($iterator));
236
    }
237
238
    /**
239
     * Return the key/pairs as a PHP generator
240
     *
241
     * @param Iterator $iterator
242
     *
243
     * @return Generator
244
     */
245 12
    protected function fetchPairsGenerator(Iterator $iterator)
246
    {
247 12
        foreach ($iterator as $row) {
248 12
            yield $row[0] => $row[1];
249 12
        }
250 12
    }
251
252
    /**
253
     * Returns a sequential array of all CSV lines;
254
     *
255
     * The rows are presented as associated arrays
256
     * The callable function will be applied to each Iterator item
257
     *
258
     * @param int|array $offset_or_keys the name for each key member OR the row Index to be
259
     *                                  used as the associated named keys
260
     *
261
     * @param callable $callable A callable to be applied to each of the rows to be returned.
262
     *
263
     * @throws InvalidArgumentException If the submitted keys are invalid
264
     *
265
     * @return Iterator|array
266
     */
267 26
    public function fetchAssoc($offset_or_keys = 0, callable $callable = null)
268
    {
269 26
        $keys = $this->getAssocKeys($offset_or_keys);
270 18
        $keys_count = count($keys);
271
        $combineArray = function (array $row) use ($keys, $keys_count) {
272 16
            if ($keys_count != count($row)) {
273 4
                $row = array_slice(array_pad($row, $keys_count, null), 0, $keys_count);
274 4
            }
275
276 16
            return array_combine($keys, $row);
277 18
        };
278 18
        $type = $this->returnType;
279 18
        $iterator = $this->fetch($combineArray);
280 18
        $iterator = $this->applyCallable($iterator, $callable);
281
282 18
        return $this->applyReturnType($type, $iterator, false);
283
    }
284
285
    /**
286
     * Selects the array to be used as key for the fetchAssoc method
287
     *
288
     * @param int|array $offset_or_keys the assoc key OR the row Index to be used
289
     *                                  as the key index
290
     *
291
     * @throws InvalidArgumentException If the row index and/or the resulting array is invalid
292
     *
293
     * @return array
294
     */
295 26
    protected function getAssocKeys($offset_or_keys)
296
    {
297 26
        if (is_array($offset_or_keys)) {
298 14
            $this->assertValidAssocKeys($offset_or_keys);
299
300 10
            return $offset_or_keys;
301
        }
302
303 12 View Code Duplication
        if (false === filter_var($offset_or_keys, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) {
304 2
            throw new InvalidArgumentException('the row index must be a positive integer, 0 or a non empty array');
305
        }
306
307 10
        $keys = $this->getRow($offset_or_keys);
308 8
        $this->assertValidAssocKeys($keys);
309 8
        $filterOutRow = function ($row, $rowIndex) use ($offset_or_keys) {
310 6
            return is_array($row) && $rowIndex != $offset_or_keys;
311 8
        };
312 8
        $this->addFilter($filterOutRow);
313
314 8
        return $keys;
315
    }
316
317
    /**
318
     * Validates the array to be used by the fetchAssoc method
319
     *
320
     * @param array $keys
321
     *
322
     * @throws InvalidArgumentException If the submitted array fails the assertion
323
     */
324 22
    protected function assertValidAssocKeys(array $keys)
325
    {
326 22
        if (empty($keys) || $keys !== array_unique(array_filter($keys, [$this, 'isValidKey']))) {
327 4
            throw new InvalidArgumentException('Use a flat array with unique string values');
328
        }
329 18
    }
330
331
    /**
332
     * Returns whether the submitted value can be used as string
333
     *
334
     * @param mixed $value
335
     *
336
     * @return bool
337
     */
338 20
    protected function isValidKey($value)
339
    {
340 20
        return is_scalar($value) || (is_object($value) && method_exists($value, '__toString'));
341
    }
342
343
    /**
344
     * Returns a single row from the CSV without filtering
345
     *
346
     * @param int $offset
347
     *
348
     * @throws InvalidArgumentException If the $offset is not valid or the row does not exist
349
     *
350
     * @return array
351
     */
352 10
    protected function getRow($offset)
353
    {
354 10
        $csv = $this->getIterator();
355 10
        $csv->setFlags($this->getFlags() & ~SplFileObject::READ_CSV);
356 10
        $iterator = new LimitIterator($csv, $offset, 1);
357 10
        $iterator->rewind();
358 10
        $res = $iterator->current();
359
360 10
        if (empty($res)) {
361 2
            throw new InvalidArgumentException('the specified row does not exist or is empty');
362
        }
363
364 8
        if (0 == $offset && $this->isBomStrippable()) {
365 4
            $res = mb_substr($res, mb_strlen($this->getInputBom()));
366 4
        }
367
368 8
        return str_getcsv($res, $this->delimiter, $this->enclosure, $this->escape);
369
    }
370
}
371