Completed
Pull Request — master (#210)
by ignace nyamagana
02:28
created

Statement::orderRecords()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.2
c 0
b 0
f 0
cc 4
eloc 11
nc 2
nop 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\Config\ValidatorTrait;
21
use LimitIterator;
22
use SplFileObject;
23
24
/**
25
 *  A trait to manage filtering a CSV
26
 *
27
 * @package League.csv
28
 * @since  9.0.0
29
 *
30
 */
31
class Statement
32
{
33
    use ValidatorTrait;
34
35
    /**
36
     * Callables to filter the iterator
37
     *
38
     * @var callable[]
39
     */
40
    protected $where = [];
41
42
    /**
43
     * Callables to sort the iterator
44
     *
45
     * @var callable[]
46
     */
47
    protected $order_by = [];
48
49
    /**
50
     * iterator Offset
51
     *
52
     * @var int
53
     */
54
    protected $offset = 0;
55
56
    /**
57
     * iterator maximum length
58
     *
59
     * @var int
60
     */
61
    protected $limit = -1;
62
63
    /**
64
     * CSV headers
65
     *
66
     * @var string[]
67
     */
68
    protected $headers = [];
69
70
    /**
71
     * Conversion Input Encoding mode in
72
     * respect to BOM encoding
73
     *
74
     * @var array
75
     */
76
    protected $bom_conversion_mode = [
77
        AbstractCsv::BOM_UTF32_BE => 'UTF-32BE',
78
        AbstractCsv::BOM_UTF32_LE => 'UTF-32LE',
79
        AbstractCsv::BOM_UTF16_BE => 'UTF-16BE',
80
        AbstractCsv::BOM_UTF16_LE => 'UTF-16LE',
81
        AbstractCsv::BOM_UTF8 => 'UTF-8',
82
    ];
83
84
    /**
85
     * Set LimitIterator Offset
86
     *
87
     * @param $offset
88
     *
89
     * @return self
90
     */
91
    public function offset(int $offset = 0): self
92
    {
93
        $this->offset = $this->filterInteger($offset, 0, 'the offset must be a positive integer or 0');
94
95
        return $this;
96
    }
97
98
    /**
99
     * Set LimitIterator Count
100
     *
101
     * @param int $limit
102
     *
103
     * @return self
104
     */
105
    public function limit(int $limit = -1): self
106
    {
107
        $this->limit = $this->filterInteger($limit, -1, 'the limit must an integer greater or equals to -1');
108
109
        return $this;
110
    }
111
112
    /**
113
     * Set an Iterator sorting callable function
114
     *
115
     * @param callable $callable
116
     *
117
     * @return self
118
     */
119
    public function orderBy(callable $callable): self
120
    {
121
        $this->order_by[] = $callable;
122
123
        return $this;
124
    }
125
126
    /**
127
     * Set the Iterator filter method
128
     *
129
     * @param callable $callable
130
     *
131
     * @return self
132
     */
133
    public function where(callable $callable): self
134
    {
135
        $this->where[] = $callable;
136
137
        return $this;
138
    }
139
140
    /**
141
     * Set the headers to be used by the RecordSet object
142
     *
143
     * @param string[] $headers
144
     *
145
     * @return self
146
     */
147
    public function headers(array $headers): self
148
    {
149
        $this->headers = $this->filterHeader($headers);
150
151
        return $this;
152
    }
153
154
    /**
155
     * Returns the inner CSV Document Iterator object
156
     *
157
     * @return RecordSet
158
     */
159
    public function process(Reader $reader)
160
    {
161
        $bom = $reader->getInputBOM();
162
        $input_encoding = $this->bom_conversion_mode[$bom] ?? 'UTF-8';
163
        $enclosure = $reader->getEnclosure();
164
        $this->prepare($reader);
165
166
        $csv = $reader->getDocument();
167
        $csv->setFlags(SplFileObject::READ_AHEAD | SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY);
168
        $iterator = $this->stripBOM($csv, $bom, $enclosure);
169
        $iterator = $this->addHeader($iterator);
170
        $iterator = $this->filterRecords($iterator);
171
        $iterator = $this->orderRecords($iterator);
172
        $records = new RecordSet(new LimitIterator($iterator, $this->offset, $this->limit), $this->headers);
173
174
        return $records->setInputEncoding($input_encoding);
175
    }
176
177
    /**
178
     * Set the computed RecordSet headers
179
     *
180
     * @param Reader $reader
181
     */
182
    protected function prepare(Reader $reader)
183
    {
184
        if (!empty($this->headers)) {
185
            return;
186
        }
187
188
        $this->headers = $reader->getHeader();
189
        $header_offset = $reader->getHeaderOffset();
190
        if (null !== $header_offset) {
191
            $strip_header = function ($row, $index) use ($header_offset) {
192
                return $index !== $header_offset;
193
            };
194
            array_unshift($this->where, $strip_header);
195
        }
196
    }
197
198
    /**
199
     * Remove the BOM sequence from the CSV
200
     *
201
     * @param Iterator $iterator
202
     *
203
     * @return Iterator
204
     */
205
    protected function stripBOM(Iterator $iterator, string $bom, string $enclosure): Iterator
206
    {
207
        if ('' == $bom) {
208
            return $iterator;
209
        }
210
211
        $bom_length = mb_strlen($bom);
212
        $strip_bom = function ($row, $index) use ($bom_length, $enclosure) {
213
            if (0 != $index || !is_array($row)) {
214
                return $row;
215
            }
216
217
            return $this->removeBOM($row, $bom_length, $enclosure);
218
        };
219
220
        return new MapIterator($iterator, $strip_bom);
221
    }
222
223
    /**
224
     * Add the CSV header if present
225
     *
226
     * @param Iterator $iterator
227
     *
228
     * @return Iterator
229
     */
230
    protected function addHeader(Iterator $iterator): Iterator
231
    {
232
        if (empty($this->headers)) {
233
            return $iterator;
234
        }
235
236
        $header_count = count($this->headers);
237
        $combine = function (array $row) use ($header_count) {
238
            if ($header_count != count($row)) {
239
                $row = array_slice(array_pad($row, $header_count, null), 0, $header_count);
240
            }
241
242
            return array_combine($this->headers, $row);
243
        };
244
245
        return new MapIterator($iterator, $combine);
246
    }
247
248
    /**
249
    * Filter the Iterator
250
    *
251
    * @param Iterator $iterator
252
    *
253
    * @return Iterator
254
    */
255
    protected function filterRecords(Iterator $iterator): Iterator
256
    {
257
        $normalized_csv = function ($row) {
258
            return is_array($row) && $row != [null];
259
        };
260
        array_unshift($this->where, $normalized_csv);
261
262
        $reducer = function ($iterator, $callable) {
263
            return new CallbackFilterIterator($iterator, $callable);
264
        };
265
266
        return array_reduce($this->where, $reducer, $iterator);
267
    }
268
269
    /**
270
    * Sort the Iterator
271
    *
272
    * @param Iterator $iterator
273
    *
274
    * @return Iterator
275
    */
276
    protected function orderRecords(Iterator $iterator): Iterator
277
    {
278
        if (empty($this->order_by)) {
279
            return $iterator;
280
        }
281
282
        $obj = new ArrayIterator(iterator_to_array($iterator));
283
        $obj->uasort(function ($row_a, $row_b) {
284
            $res = 0;
285
            foreach ($this->order_by as $compare) {
286
                if (0 !== ($res = $compare($row_a, $row_b))) {
287
                    break;
288
                }
289
            }
290
291
            return $res;
292
        });
293
294
        return $obj;
295
    }
296
}
297