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