Completed
Push — master ( 15957b...0cdac0 )
by ignace nyamagana
05:37 queued 02:59
created

Statement::buildOrderBy()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 11
cts 11
cp 1
rs 9.0534
c 0
b 0
f 0
cc 4
eloc 11
nc 2
nop 1
crap 4
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 24
    public function columns(array $columns): self
81
    {
82 24
        $columns = $this->filterColumnNames($columns);
83 22
        if ($columns === $this->columns) {
84 2
            return $this;
85
        }
86
87 20
        $clone = clone $this;
88 20
        $clone->columns = $columns;
89
90 20
        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, __METHOD__.': 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, __METHOD__.': 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 118
    public function process(Reader $reader): RecordSet
171
    {
172 118
        list($columns, $combine) = $this->buildColumns($reader->getHeader());
173 114
        $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 114
        $iterator = $this->buildOrderBy($iterator);
175 114
        $iterator = new LimitIterator($iterator, $this->offset, $this->limit);
176 114
        if (null !== $combine) {
177 18
            $iterator = new MapIterator($iterator, $combine);
178
        }
179
180 114
        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 116
    protected function buildColumns(array $columns): array
191
    {
192 116
        $combine = null;
193 116
        if (!empty($this->columns)) {
194 20
            $columns_alias = $this->filterColumnAgainstCsvHeader($columns);
195 18
            $columns = array_values($columns_alias);
196
            $combine = function (array $record) use ($columns_alias): array {
197 10
                $res = [];
198 10
                foreach ($columns_alias as $key => $alias) {
199 10
                    $res[$alias] = $record[$key] ?? null;
200
                }
201
202 10
                return $res;
203 18
            };
204
        }
205
206 114
        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 20
    protected function filterColumnAgainstCsvHeader(array $headers)
217
    {
218 20
        if (empty($headers)) {
219
            $filter = function ($key): bool {
220 18
                return !is_int($key) || $key < 0;
221 9
            };
222
223 18
            if (empty(array_filter($this->columns, $filter, ARRAY_FILTER_USE_KEY))) {
224 16
                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 1
                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 114
    protected function buildWhere(Iterator $iterator): Iterator
265
    {
266
        $reducer = function (Iterator $iterator, callable $callable): Iterator {
267 2
            return new CallbackFilterIterator($iterator, $callable);
268 57
        };
269
270 114
        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 114
    protected function buildOrderBy(Iterator $iterator): Iterator
281
    {
282 114
        if (empty($this->order_by)) {
283 112
            return $iterator;
284
        }
285
286 2
        $compare = function (array $record_a, array $record_b): int {
287 2
            foreach ($this->order_by as $callable) {
288 2
                if (0 !== ($cmp = $callable($record_a, $record_b))) {
289 2
                    return $cmp;
290
                }
291
            }
292
293
            return $cmp ?? 0;
294 1
        };
295
296 2
        $iterator = new ArrayIterator(iterator_to_array($iterator, true));
297 2
        $iterator->uasort($compare);
298
299 2
        return $iterator;
300
    }
301
}
302