1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace EasyCSV; |
||
6 | |||
7 | use LogicException; |
||
8 | use function array_combine; |
||
9 | use function array_filter; |
||
10 | use function count; |
||
11 | use function is_array; |
||
12 | use function is_string; |
||
13 | use function mb_strpos; |
||
14 | use function sprintf; |
||
15 | use function str_getcsv; |
||
16 | use function str_replace; |
||
17 | |||
18 | class Reader extends AbstractBase |
||
19 | { |
||
20 | /** @var bool */ |
||
21 | private $headersInFirstRow = true; |
||
22 | |||
23 | /** @var string[]|bool */ |
||
24 | private $headers = false; |
||
25 | |||
26 | /** @var bool */ |
||
27 | private $init; |
||
28 | |||
29 | /** @var bool|int */ |
||
30 | private $headerLine = false; |
||
31 | |||
32 | /** @var bool|int */ |
||
33 | private $lastLine = false; |
||
34 | |||
35 | /** @var bool */ |
||
36 | private $isNeedBOMRemove = true; |
||
37 | |||
38 | 4 | public function __construct(string $path, string $mode = 'r+', bool $headersInFirstRow = true) |
|
39 | { |
||
40 | 4 | parent::__construct($path, $mode); |
|
41 | |||
42 | 4 | $this->headersInFirstRow = $headersInFirstRow; |
|
43 | 4 | } |
|
44 | |||
45 | /** |
||
46 | * @return string[]|bool |
||
47 | */ |
||
48 | 6 | public function getHeaders() |
|
49 | { |
||
50 | 6 | $this->init(); |
|
51 | |||
52 | 6 | return $this->headers; |
|
53 | } |
||
54 | |||
55 | /** |
||
56 | * @return mixed[]|bool |
||
57 | */ |
||
58 | 26 | public function getRow() |
|
59 | { |
||
60 | 26 | $this->init(); |
|
61 | |||
62 | 26 | if ($this->isEof()) { |
|
63 | 10 | return false; |
|
64 | } |
||
65 | |||
66 | 26 | $row = $this->getCurrentRow(); |
|
67 | 26 | $isEmpty = $this->rowIsEmpty($row); |
|
68 | |||
69 | 26 | if ($this->isEof() === false) { |
|
0 ignored issues
–
show
introduced
by
![]() |
|||
70 | 26 | $this->getHandle()->next(); |
|
71 | } |
||
72 | |||
73 | 26 | if ($isEmpty === false) { |
|
74 | 26 | return $this->headers !== false && is_array($this->headers) ? array_combine($this->headers, $row) : $row; |
|
75 | } |
||
76 | |||
77 | 4 | if ((isset($this->headers) && is_array($this->headers)) && (count($this->headers) !== count($row))) { |
|
78 | 4 | return $this->getRow(); |
|
79 | } |
||
80 | |||
81 | 2 | if (is_array($this->headers)) { |
|
82 | 2 | return array_combine($this->headers, $row); |
|
83 | } |
||
84 | |||
85 | return false; |
||
86 | } |
||
87 | |||
88 | 30 | public function isEof() : bool |
|
89 | { |
||
90 | 30 | return $this->getHandle()->eof(); |
|
91 | } |
||
92 | |||
93 | /** |
||
94 | * @return mixed[] |
||
95 | */ |
||
96 | 8 | public function getAll() : array |
|
97 | { |
||
98 | 8 | $data = []; |
|
99 | 8 | while ($row = $this->getRow()) { |
|
100 | 8 | $data[] = $row; |
|
101 | } |
||
102 | |||
103 | 8 | return $data; |
|
104 | } |
||
105 | |||
106 | 6 | public function getLineNumber() : int |
|
107 | { |
||
108 | 6 | return $this->getHandle()->key(); |
|
109 | } |
||
110 | |||
111 | /** |
||
112 | * @return int|bool |
||
113 | */ |
||
114 | 4 | public function getLastLineNumber() |
|
115 | { |
||
116 | 4 | if ($this->lastLine !== false) { |
|
117 | 2 | return $this->lastLine; |
|
118 | } |
||
119 | |||
120 | 4 | $this->getHandle()->seek($this->getHandle()->getSize()); |
|
121 | 4 | $lastLine = $this->getHandle()->key(); |
|
122 | |||
123 | 4 | $this->getHandle()->rewind(); |
|
124 | |||
125 | 4 | return $this->lastLine = $lastLine; |
|
126 | } |
||
127 | |||
128 | /** |
||
129 | * @return string[] |
||
130 | */ |
||
131 | 28 | public function getCurrentRow() : array |
|
132 | { |
||
133 | 28 | $current = $this->getHandle()->current(); |
|
134 | |||
135 | 28 | if (! is_string($current)) { |
|
136 | return []; |
||
137 | } |
||
138 | |||
139 | 28 | if ($this->isNeedBOMRemove && mb_strpos($current, "\xEF\xBB\xBF", 0, 'utf-8') === 0) { |
|
140 | 2 | $this->isNeedBOMRemove = false; |
|
141 | |||
142 | 2 | $current = str_replace("\xEF\xBB\xBF", '', $current); |
|
143 | } |
||
144 | |||
145 | 28 | return str_getcsv($current, $this->delimiter, $this->enclosure); |
|
146 | } |
||
147 | |||
148 | 12 | public function advanceTo(int $lineNumber) : void |
|
149 | { |
||
150 | 12 | if ($this->headerLine > $lineNumber) { |
|
151 | 2 | throw new LogicException(sprintf( |
|
152 | 2 | 'Line Number %s is before the header line that was set', |
|
153 | 2 | $lineNumber |
|
154 | )); |
||
155 | } |
||
156 | |||
157 | 10 | if ($this->headerLine === $lineNumber) { |
|
158 | 2 | throw new LogicException(sprintf( |
|
159 | 2 | 'Line Number %s is equal to the header line that was set', |
|
160 | 2 | $lineNumber |
|
161 | )); |
||
162 | } |
||
163 | |||
164 | 8 | if ($lineNumber > 0) { |
|
165 | 8 | $this->getHandle()->seek($lineNumber - 1); |
|
166 | } // check the line before |
||
167 | |||
168 | 8 | if ($this->isEof()) { |
|
169 | 2 | throw new LogicException(sprintf( |
|
170 | 2 | 'Line Number %s is past the end of the file', |
|
171 | 2 | $lineNumber |
|
172 | )); |
||
173 | } |
||
174 | |||
175 | 6 | $this->getHandle()->seek($lineNumber); |
|
176 | 6 | } |
|
177 | |||
178 | 6 | public function setHeaderLine(int $lineNumber) : bool |
|
179 | { |
||
180 | 6 | if ($lineNumber === 0) { |
|
181 | 2 | return false; |
|
182 | } |
||
183 | |||
184 | 4 | $this->headersInFirstRow = false; |
|
185 | |||
186 | 4 | $this->headerLine = $lineNumber; |
|
187 | |||
188 | 4 | $this->getHandle()->seek($lineNumber); |
|
189 | |||
190 | // get headers |
||
191 | 4 | $this->headers = $this->getHeadersFromRow(); |
|
192 | |||
193 | 4 | return true; |
|
194 | } |
||
195 | |||
196 | 26 | protected function init() : void |
|
197 | { |
||
198 | 26 | if ($this->init === true) { |
|
199 | 24 | return; |
|
200 | } |
||
201 | |||
202 | 26 | $this->init = true; |
|
203 | |||
204 | 26 | if ($this->headersInFirstRow !== true) { |
|
205 | 6 | return; |
|
206 | } |
||
207 | |||
208 | 20 | $this->getHandle()->rewind(); |
|
209 | |||
210 | 20 | $this->headerLine = 0; |
|
211 | |||
212 | 20 | $this->headers = $this->getHeadersFromRow(); |
|
213 | 20 | } |
|
214 | |||
215 | /** |
||
216 | * @param string[]|null[] $row |
||
217 | */ |
||
218 | 26 | protected function rowIsEmpty(array $row) : bool |
|
219 | { |
||
220 | 26 | $emptyRow = ($row === [null]); |
|
221 | 26 | $emptyRowWithDelimiters = (array_filter($row) === []); |
|
222 | 26 | $isEmpty = false; |
|
223 | |||
224 | 26 | if ($emptyRow) { |
|
225 | 4 | return true; |
|
226 | } |
||
227 | |||
228 | 26 | if ($emptyRowWithDelimiters) { |
|
229 | 2 | return true; |
|
230 | } |
||
231 | |||
232 | 26 | return $isEmpty; |
|
233 | } |
||
234 | |||
235 | /** |
||
236 | * @return string[] |
||
237 | */ |
||
238 | 24 | private function getHeadersFromRow() : array |
|
239 | { |
||
240 | 24 | $row = $this->getRow(); |
|
241 | |||
242 | 24 | return is_array($row) ? $row : []; |
|
243 | } |
||
244 | } |
||
245 |