Completed
Pull Request — master (#210)
by ignace nyamagana
12:18
created

Statement::orderBy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
crap 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 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
declare(strict_types=1);
14
15
namespace League\Csv;
16
17
use ArrayIterator;
18
use CallbackFilterIterator;
19
use Iterator;
20
use League\Csv\Exception\RuntimeException;
21
use LimitIterator;
22
23
/**
24
 *  A trait to manage filtering a CSV
25
 *
26
 * @package League.csv
27
 * @since   9.0.0
28
 * @author  Ignace Nyamagana Butera <[email protected]>
29
 *
30
 */
31
class Statement
32
{
33
    use ValidatorTrait;
34
35
    /**
36
     * CSV columns name
37
     *
38
     * @var array
39
     */
40
    protected $columns = [];
41
42
    /**
43
     * Callables to filter the iterator
44
     *
45
     * @var callable[]
46
     */
47
    protected $where = [];
48
49
    /**
50
     * Callables to sort the iterator
51
     *
52
     * @var callable[]
53
     */
54
    protected $order_by = [];
55
56
    /**
57
     * iterator Offset
58
     *
59
     * @var int
60
     */
61
    protected $offset = 0;
62
63
    /**
64
     * iterator maximum length
65
     *
66
     * @var int
67
     */
68
    protected $limit = -1;
69
70
    /**
71
     * Set and selected columns to be used by the RecordSet object
72
     *
73
     * The array offset represents the CSV document header value
74
     * The array value represents the Alias named to be used by the RecordSet object
75
     *
76
     * @param array $columns
77
     *
78
     * @return self
79
     */
80 20
    public function columns(array $columns): self
81
    {
82 20
        $columns = $this->filterColumnNames($columns);
83 18
        if ($columns === $this->columns) {
84 2
            return $this;
85
        }
86
87 16
        $clone = clone $this;
88 16
        $clone->columns = $columns;
89
90 16
        return $clone;
91
    }
92
93
    /**
94
     * Set the Iterator filter method
95
     *
96
     * @param callable $callable
97
     *
98
     * @return self
99
     */
100 2
    public function where(callable $callable): self
101
    {
102 2
        $clone = clone $this;
103 2
        $clone->where[] = $callable;
104
105 2
        return $clone;
106
    }
107
108
    /**
109
     * Set an Iterator sorting callable function
110
     *
111
     * @param callable $callable
112
     *
113
     * @return self
114
     */
115 2
    public function orderBy(callable $callable): self
116
    {
117 2
        $clone = clone $this;
118 2
        $clone->order_by[] = $callable;
119
120 2
        return $clone;
121
    }
122
123
    /**
124
     * Set LimitIterator Offset
125
     *
126
     * @param $offset
127
     *
128
     * @return self
129
     */
130 12
    public function offset(int $offset): self
131
    {
132 12
        $offset = $this->filterInteger($offset, 0, 'the offset must be a positive integer or 0');
133 12
        if ($offset === $this->offset) {
134 2
            return $this;
135
        }
136
137 10
        $clone = clone $this;
138 10
        $clone->offset = $offset;
139
140 10
        return $clone;
141
    }
142
143
    /**
144
     * Set LimitIterator Count
145
     *
146
     * @param int $limit
147
     *
148
     * @return self
149
     */
150 16
    public function limit(int $limit): self
151
    {
152 16
        $limit = $this->filterInteger($limit, -1, 'the limit must an integer greater or equals to -1');
153 14
        if ($limit === $this->limit) {
154 2
            return $this;
155
        }
156
157 12
        $clone = clone $this;
158 12
        $clone->limit = $limit;
159
160 12
        return $clone;
161
    }
162
163
    /**
164
     * Returns the inner CSV Document Iterator object
165
     *
166
     * @param Reader $reader
167
     *
168
     * @return RecordSet
169
     */
170 112
    public function process(Reader $reader): RecordSet
171
    {
172 112
        list($columns, $combine) = $this->buildColumns($reader->getHeader());
173 108
        $iterator = $this->buildWhere($reader->getIterator());
0 ignored issues
show
Compatibility introduced by
$reader->getIterator() 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...
174 108
        $iterator = $this->buildOrderBy($iterator);
175 108
        $iterator = new LimitIterator($iterator, $this->offset, $this->limit);
176 108
        if (null !== $combine) {
177 14
            $iterator = new MapIterator($iterator, $combine);
178
        }
179
180 108
        return new RecordSet($iterator, $columns);
181
    }
182
183
    /**
184
     * Add the CSV column if present
185
     *
186
     * @param string[] $columns
187
     *
188
     * @return array
189
     */
190 110
    protected function buildColumns(array $columns): array
191
    {
192 110
        $combine = null;
193 110
        if (!empty($this->columns)) {
194 16
            $columns_alias = $this->filterColumnAgainstCsvHeader($columns);
195 14
            $columns = array_values($columns_alias);
196
            $combine = function (array $row) use ($columns_alias): array {
197 10
                $record = [];
198 10
                foreach ($columns_alias as $key => $alias) {
199 10
                    $record[$alias] = $row[$key] ?? null;
200
                }
201
202 10
                return $record;
203 14
            };
204
        }
205
206 108
        return [$columns, $combine];
207
    }
208
209
    /**
210
     * Validate the column against the processed CSV header
211
     *
212
     * @param string[] $headers Reader CSV header
213
     *
214
     * @throws RuntimeException If a column is not found
215
     */
216 16
    protected function filterColumnAgainstCsvHeader(array $headers)
217
    {
218 16
        if (empty($headers)) {
219
            $filter = function ($key) {
220 14
                return !is_int($key) || $key < 0;
221 14
            };
222
223 14
            if (empty(array_filter($this->columns, $filter, ARRAY_FILTER_USE_KEY))) {
224 12
                return $this->columns;
225
            }
226
227 2
            throw new RuntimeException('If no header is specified the columns keys must contain only integer');
228
        }
229
230 2
        $columns = $this->formatColumns($this->columns);
231 2
        foreach ($columns as $key => $alias) {
232 2
            if (false === array_search($key, $headers, true)) {
233 2
                throw new RuntimeException(sprintf('The `%s` column does not exist in the Csv document', $key));
234
            }
235
        }
236
237 2
        return $columns;
238
    }
239
240
    /**
241
     * Format the column array
242
     *
243
     * @param array $columns
244
     *
245
     * @return array
246
     */
247 2
    private function formatColumns(array $columns): array
248
    {
249 2
        $res = [];
250 2
        foreach ($columns as $key => $alias) {
251 2
            $res[!is_string($key) ? $alias : $key] = $alias;
252
        }
253
254 2
        return $res;
255
    }
256
257
    /**
258
    * Filter the Iterator
259
    *
260
    * @param Iterator $iterator
261
    *
262
    * @return Iterator
263
    */
264 108
    protected function buildWhere(Iterator $iterator): Iterator
265
    {
266
        $reducer = function (Iterator $iterator, callable $callable): Iterator {
267 2
            return new CallbackFilterIterator($iterator, $callable);
268 108
        };
269
270 108
        return array_reduce($this->where, $reducer, $iterator);
271
    }
272
273
    /**
274
    * Sort the Iterator
275
    *
276
    * @param Iterator $iterator
277
    *
278
    * @return Iterator
279
    */
280 108
    protected function buildOrderBy(Iterator $iterator): Iterator
281
    {
282 108
        if (empty($this->order_by)) {
283 106
            return $iterator;
284
        }
285
286 2
        $compare = function (array $record_a, array $record_b): int {
287 2
            $res = 0;
288 2
            foreach ($this->order_by as $callable) {
289 2
                if (0 !== ($res = $callable($record_a, $record_b))) {
290 2
                    break;
291
                }
292
            }
293
294 2
            return $res;
295 2
        };
296
297 2
        $iterator = new ArrayIterator(iterator_to_array($iterator, true));
298 2
        $iterator->uasort($compare);
299
300 2
        return $iterator;
301
    }
302
}
303