Completed
Pull Request — master (#210)
by ignace nyamagana
06:26
created

Statement::buildOrderBy()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

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