PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.2.2
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.2.2
2.4.0 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 All 67 releases
better-payment / vendor / league / csv / src / Reader.php

Reader.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.2.2, at vendor/league/csv/src/Reader.php

366 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * League.Csv (https://csv.thephpleague.com)
5 *
6 * (c) Ignace Nyamagana Butera <[email protected]>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 declare(strict_types=1);
13
14 namespace League\Csv;
15
16 use CallbackFilterIterator;
17 use Iterator;
18 use JsonSerializable;
19 use SplFileObject;
20 use function array_combine;
21 use function array_filter;
22 use function array_pad;
23 use function array_slice;
24 use function array_unique;
25 use function count;
26 use function is_array;
27 use function iterator_count;
28 use function iterator_to_array;
29 use function mb_strlen;
30 use function mb_substr;
31 use function strlen;
32 use function substr;
33 use const STREAM_FILTER_READ;
34
35 /**
36 * A class to parse and read records from a CSV document.
37 */
38 class Reader extends AbstractCsv implements TabularDataReader, JsonSerializable
39 {
40 protected const STREAM_FILTER_MODE = STREAM_FILTER_READ;
41
42 protected ?int $header_offset = null;
43 protected int $nb_records = -1;
44 protected bool $is_empty_records_included = false;
45 /** @var array<string> header record. */
46 protected array $header = [];
47
48 public static function createFromPath(string $path, string $open_mode = 'r', $context = null)
49 {
50 return parent::createFromPath($path, $open_mode, $context);
51 }
52
53 protected function resetProperties(): void
54 {
55 $this->nb_records = -1;
56 $this->header = [];
57 }
58
59 /** Returns the header offset. */
60 public function getHeaderOffset(): ?int
61 {
62 return $this->header_offset;
63 }
64
65 public function getHeader(): array
66 {
67 if (null === $this->header_offset) {
68 return $this->header;
69 }
70
71 if ([] !== $this->header) {
72 return $this->header;
73 }
74
75 $this->header = $this->setHeader($this->header_offset);
76
77 return $this->header;
78 }
79
80 /**
81 * Determine the CSV record header.
82 *
83 * @throws Exception If the header offset is set and no record is found or is the empty array
84 *
85 * @return array<string>
86 */
87 protected function setHeader(int $offset): array
88 {
89 $header = $this->seekRow($offset);
90 if (in_array($header, [[], [null]], true)) {
91 throw SyntaxError::dueToHeaderNotFound($offset);
92 }
93
94 if (0 !== $offset) {
95 return $header;
96 }
97
98 $header = $this->removeBOM($header, mb_strlen($this->getInputBOM()), $this->enclosure);
99 if ([''] === $header) {
100 throw SyntaxError::dueToHeaderNotFound($offset);
101 }
102
103 return $header;
104 }
105
106 /** Returns the row at a given offset. */
107 protected function seekRow(int $offset): array
108 {
109 foreach ($this->getDocument() as $index => $record) {
110 if ($offset === $index) {
111 return $record;
112 }
113 }
114
115 return [];
116 }
117
118 /**
119 * Returns the document as an Iterator.
120 */
121 protected function getDocument(): Iterator
122 {
123 $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD);
124 $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
125 $this->document->rewind();
126
127 return $this->document;
128 }
129
130 /**
131 * Strip the BOM sequence from a record.
132 *
133 * @param string[] $record
134 *
135 * @return array<string>
136 */
137 protected function removeBOM(array $record, int $bom_length, string $enclosure): array
138 {
139 if (0 === $bom_length) {
140 return $record;
141 }
142
143 $record[0] = mb_substr($record[0], $bom_length);
144 if ($enclosure.$enclosure != substr($record[0].$record[0], strlen($record[0]) - 1, 2)) {
145 return $record;
146 }
147
148 $record[0] = substr($record[0], 1, -1);
149
150 return $record;
151 }
152
153 public function fetchColumnByName(string $name): Iterator
154 {
155 return ResultSet::createFromTabularDataReader($this)->fetchColumnByName($name);
156 }
157
158 public function fetchColumnByOffset(int $offset = 0): Iterator
159 {
160 return ResultSet::createFromTabularDataReader($this)->fetchColumnByOffset($offset);
161 }
162
163 public function fetchColumn($index = 0): Iterator
164 {
165 return ResultSet::createFromTabularDataReader($this)->fetchColumn($index);
166 }
167
168 public function fetchOne(int $nth_record = 0): array
169 {
170 return ResultSet::createFromTabularDataReader($this)->fetchOne($nth_record);
171 }
172
173 public function fetchPairs($offset_index = 0, $value_index = 1): Iterator
174 {
175 return ResultSet::createFromTabularDataReader($this)->fetchPairs($offset_index, $value_index);
176 }
177
178 public function count(): int
179 {
180 if (-1 === $this->nb_records) {
181 $this->nb_records = iterator_count($this->getRecords());
182 }
183
184 return $this->nb_records;
185 }
186
187 public function getIterator(): Iterator
188 {
189 return $this->getRecords();
190 }
191
192 public function jsonSerialize(): array
193 {
194 return iterator_to_array($this->getRecords(), false);
195 }
196
197 public function getRecords(array $header = []): Iterator
198 {
199 $header = $this->computeHeader($header);
200 $normalized = fn ($record): bool => is_array($record) && ($this->is_empty_records_included || $record != [null]);
201
202 $bom = '';
203 if (!$this->is_input_bom_included) {
204 $bom = $this->getInputBOM();
205 }
206
207 $document = $this->getDocument();
208 $records = $this->stripBOM(new CallbackFilterIterator($document, $normalized), $bom);
209 if (null !== $this->header_offset) {
210 $records = new CallbackFilterIterator($records, fn (array $record, int $offset): bool => $offset !== $this->header_offset);
211 }
212
213 if ($this->is_empty_records_included) {
214 return $this->combineHeader(new MapIterator(
215 $records,
216 fn (array $record): array => ([null] === $record) ? [] : $record
217 ), $header);
218 }
219
220 return $this->combineHeader($records, $header);
221 }
222
223 /**
224 * Returns the header to be used for iteration.
225 *
226 * @param string[] $header
227 *
228 * @throws Exception If the header contains non unique column name
229 *
230 * @return array<string>
231 */
232 protected function computeHeader(array $header)
233 {
234 if ([] === $header) {
235 $header = $this->getHeader();
236 }
237
238 if ($header !== ($filtered_header = array_filter($header, 'is_string'))) {
239 throw SyntaxError::dueToInvalidHeaderColumnNames();
240 }
241
242 if ($header !== array_unique($filtered_header)) {
243 throw SyntaxError::dueToDuplicateHeaderColumnNames($header);
244 }
245
246 return $header;
247 }
248
249 /**
250 * Combine the CSV header to each record if present.
251 *
252 * @param string[] $header
253 */
254 protected function combineHeader(Iterator $iterator, array $header): Iterator
255 {
256 if ([] === $header) {
257 return $iterator;
258 }
259
260 $field_count = count($header);
261 $mapper = static function (array $record) use ($header, $field_count): array {
262 if (count($record) != $field_count) {
263 $record = array_slice(array_pad($record, $field_count, null), 0, $field_count);
264 }
265
266 /** @var array<string|null> $assocRecord */
267 $assocRecord = array_combine($header, $record);
268
269 return $assocRecord;
270 };
271
272 return new MapIterator($iterator, $mapper);
273 }
274
275 /**
276 * Strip the BOM sequence from the returned records if necessary.
277 */
278 protected function stripBOM(Iterator $iterator, string $bom): Iterator
279 {
280 if ('' === $bom) {
281 return $iterator;
282 }
283
284 $bom_length = mb_strlen($bom);
285 $mapper = function (array $record, int $index) use ($bom_length): array {
286 if (0 !== $index) {
287 return $record;
288 }
289
290 $record = $this->removeBOM($record, $bom_length, $this->enclosure);
291 if ([''] === $record) {
292 return [null];
293 }
294
295 return $record;
296 };
297
298 return new CallbackFilterIterator(
299 new MapIterator($iterator, $mapper),
300 fn (array $record): bool => $this->is_empty_records_included || $record != [null]
301 );
302 }
303
304 /**
305 * Selects the record to be used as the CSV header.
306 *
307 * Because the header is represented as an array, to be valid
308 * a header MUST contain only unique string value.
309 *
310 * @param int|null $offset the header record offset
311 *
312 * @throws Exception if the offset is a negative integer
313 *
314 * @return static
315 */
316 public function setHeaderOffset(?int $offset): self
317 {
318 if ($offset === $this->header_offset) {
319 return $this;
320 }
321
322 if (null !== $offset && 0 > $offset) {
323 throw InvalidArgument::dueToInvalidHeaderOffset($offset, __METHOD__);
324 }
325
326 $this->header_offset = $offset;
327 $this->resetProperties();
328
329 return $this;
330 }
331
332 /**
333 * Enable skipping empty records.
334 */
335 public function skipEmptyRecords(): self
336 {
337 if ($this->is_empty_records_included) {
338 $this->is_empty_records_included = false;
339 $this->nb_records = -1;
340 }
341
342 return $this;
343 }
344
345 /**
346 * Disable skipping empty records.
347 */
348 public function includeEmptyRecords(): self
349 {
350 if (!$this->is_empty_records_included) {
351 $this->is_empty_records_included = true;
352 $this->nb_records = -1;
353 }
354
355 return $this;
356 }
357
358 /**
359 * Tells whether empty records are skipped by the instance.
360 */
361 public function isEmptyRecordsIncluded(): bool
362 {
363 return $this->is_empty_records_included;
364 }
365 }
366