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