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

Statement::stripBOM()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

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