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