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

Reader::setHeaderOffset()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 8
nc 2
nop 1
crap 2
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 CallbackFilterIterator;
18
use Iterator;
19
use IteratorAggregate;
20
use LimitIterator;
21
use SplFileObject;
22
23
/**
24
 *  A class to manage extracting and filtering a CSV
25
 *
26
 * @package League.csv
27
 * @since  3.0.0
28
 *
29
 */
30
class Reader extends AbstractCsv implements IteratorAggregate
31
{
32
    /**
33
     * @inheritdoc
34
     */
35
    protected $stream_filter_mode = STREAM_FILTER_READ;
36
37
    /**
38
     * CSV Document header offset
39
     *
40
     * @var int|null
41
     */
42
    protected $header_offset;
43
44
    /**
45
     * Returns the record offset used as header
46
     *
47
     * If no CSV record is used this method MUST return null
48
     *
49
     * @return int|null
50
     */
51 2
    public function getHeaderOffset()
52
    {
53 2
        return $this->header_offset;
54
    }
55
56
    /**
57
     * Selects the record to be used as the CSV header
58
     *
59
     * Because of the header is represented as an array, to be valid
60
     * a header MUST contain only unique string value.
61
     *
62
     * @param int|null $offset the header row offset
63
     *
64
     * @return static
65
     */
66 26
    public function setHeaderOffset($offset): self
67
    {
68 26
        $this->header_offset = null;
69 26
        if (null !== $offset) {
70 22
            $this->header_offset = $this->filterInteger(
71
                $offset,
72 22
                0,
73 22
                'the header offset index must be a positive integer or 0'
74
            );
75
        }
76
77 24
        return $this;
78
    }
79
80
    /**
81
     * Detect Delimiters occurences in the CSV
82
     *
83
     * Returns a associative array where each key represents
84
     * a valid delimiter and each value the number of occurences
85
     *
86
     * @param string[] $delimiters the delimiters to consider
87
     * @param int      $nb_rows    Detection is made using $nb_rows of the CSV
88
     *
89
     * @return array
90
     */
91 8
    public function fetchDelimitersOccurrence(array $delimiters, int $nb_rows = 1): array
92
    {
93 8
        $nb_rows = $this->filterInteger($nb_rows, 1, 'The number of rows to consider must be a valid positive integer');
94
        $filter_row = function ($row) {
95 6
            return is_array($row) && count($row) > 1;
96 6
        };
97
        $delimiters = array_unique(array_filter($delimiters, function ($value) {
98 6
            return 1 == strlen($value);
99 6
        }));
100 6
        $this->document->setFlags(SplFileObject::READ_CSV);
101 6
        $res = [];
102 6
        foreach ($delimiters as $delim) {
103 6
            $this->document->setCsvControl($delim, $this->enclosure, $this->escape);
104 6
            $iterator = new CallbackFilterIterator(new LimitIterator($this->document, 0, $nb_rows), $filter_row);
105 6
            $res[$delim] = count(iterator_to_array($iterator, false), COUNT_RECURSIVE);
106
        }
107 6
        arsort($res, SORT_NUMERIC);
108
109 6
        return $res;
110
    }
111
112
    /**
113
     * Returns a collection of selected records
114
     *
115
     * @param Statement|null $stmt
116
     *
117
     * @return RecordSet
118
     */
119 78
    public function select(Statement $stmt = null): RecordSet
120
    {
121 78
        if (null === $stmt) {
122 70
            $stmt = new Statement();
123
        }
124
125 78
        return $stmt->process($this);
126
    }
127
128
    /**
129
     * @inheritdoc
130
     */
131 108
    public function getIterator(): Iterator
132
    {
133 108
        $bom = $this->getInputBOM();
134 108
        $header = $this->getHeader();
135 108
        $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
136 108
        $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
137
        $iterator = new CallbackFilterIterator($this->document, function ($row) {
138 96
            return is_array($row) && $row != [null];
139 108
        });
140
141 108
        return $this->stripBOM($this->combineHeader($iterator, $header), $bom);
142
    }
143
144
    /**
145
     * Add the CSV header if present and valid
146
     *
147
     * @param Iterator $iterator
148
     * @param string[] $header
149
     *
150
     * @return Iterator
151
     */
152 108
    protected function combineHeader(Iterator $iterator, array $header): Iterator
153
    {
154 108
        if (null === $this->header_offset) {
155 92
            return $iterator;
156
        }
157
158 16
        $header = $this->filterHeader($header);
159 16
        $header_count = count($header);
160
        $iterator = new CallbackFilterIterator($iterator, function (array $row, int $offset) {
161 12
            return $offset != $this->header_offset;
162 16
        });
163
164
        return new MapIterator($iterator, function (array $row) use ($header_count, $header) {
165 12
            if ($header_count != count($row)) {
166
                $row = array_slice(array_pad($row, $header_count, null), 0, $header_count);
167
            }
168
169 12
            return array_combine($header, $row);
170 16
        });
171
    }
172
173
    /**
174
     * Strip the BOM sequence if present
175
     *
176
     * @param Iterator $iterator
177
     * @param string   $bom
178
     *
179
     * @return Iterator
180
     */
181 108
    protected function stripBOM(Iterator $iterator, string $bom): Iterator
182
    {
183 108
        if ('' === $bom) {
184 90
            return $iterator;
185
        }
186
187 18
        $bom_length = mb_strlen($bom);
188 18
        return new MapIterator($iterator, function (array $row, $index) use ($bom_length) {
189 18
            if (0 != $index) {
190 14
                return $row;
191
            }
192
193 14
            return $this->removeBOM($row, $bom_length, $this->enclosure);
194 18
        });
195
    }
196
197
    /**
198
     * Returns the column header associate with the RecordSet
199
     *
200
     * @throws Exception If no header is found
201
     *
202
     * @return string[]
203
     */
204 112
    public function getHeader(): array
205
    {
206 112
        if (null === $this->header_offset) {
207 94
            return [];
208
        }
209
210 20
        $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
211 20
        $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
212 20
        $this->document->seek($this->header_offset);
213 20
        $header = $this->document->current();
214 20
        if (empty($header)) {
215 2
            throw new Exception('The header record specified by `Reader::setHeaderOffset` does not exist or is empty');
216
        }
217
218 18
        if (0 !== $this->header_offset) {
219 4
            return $header;
220
        }
221
222 14
        return $this->removeBOM($header, mb_strlen($this->getInputBOM()), $this->enclosure);
223
    }
224
}
225