PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.2
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.2
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
fluent-cart / vendor / openspout / openspout / src / Reader / XLSX / RowIterator.php

RowIterator.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.4.2, at vendor/openspout/openspout/src/Reader/XLSX/RowIterator.php

353 lines 15.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\OpenSpout\Reader\XLSX;
4
5 use FluentCart\OpenSpout\Common\Entity\Cell;
6 use FluentCart\OpenSpout\Common\Entity\Row;
7 use FluentCart\OpenSpout\Common\Exception\IOException;
8 use FluentCart\OpenSpout\Reader\Common\Manager\RowManager;
9 use FluentCart\OpenSpout\Reader\Common\XMLProcessor;
10 use FluentCart\OpenSpout\Reader\Exception\InvalidValueException;
11 use FluentCart\OpenSpout\Reader\Exception\XMLProcessingException;
12 use FluentCart\OpenSpout\Reader\IteratorInterface;
13 use FluentCart\OpenSpout\Reader\Wrapper\XMLReader;
14 use FluentCart\OpenSpout\Reader\XLSX\Creator\InternalEntityFactory;
15 use FluentCart\OpenSpout\Reader\XLSX\Helper\CellHelper;
16 use FluentCart\OpenSpout\Reader\XLSX\Helper\CellValueFormatter;
17 class RowIterator implements IteratorInterface
18 {
19 /** Definition of XML nodes names used to parse data */
20 public const XML_NODE_DIMENSION = 'dimension';
21 public const XML_NODE_WORKSHEET = 'worksheet';
22 public const XML_NODE_ROW = 'row';
23 public const XML_NODE_CELL = 'c';
24 /** Definition of XML attributes used to parse data */
25 public const XML_ATTRIBUTE_REF = 'ref';
26 public const XML_ATTRIBUTE_SPANS = 'spans';
27 public const XML_ATTRIBUTE_ROW_INDEX = 'r';
28 public const XML_ATTRIBUTE_CELL_INDEX = 'r';
29 /** @var string Path of the XLSX file being read */
30 protected $filePath;
31 /** @var string Path of the sheet data XML file as in [Content_Types].xml */
32 protected $sheetDataXMLFilePath;
33 /** @var \OpenSpout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
34 protected $xmlReader;
35 /** @var \OpenSpout\Reader\Common\XMLProcessor Helper Object to process XML nodes */
36 protected $xmlProcessor;
37 /** @var Helper\CellValueFormatter Helper to format cell values */
38 protected $cellValueFormatter;
39 /** @var \OpenSpout\Reader\Common\Manager\RowManager Manages rows */
40 protected $rowManager;
41 /** @var \OpenSpout\Reader\XLSX\Creator\InternalEntityFactory Factory to create entities */
42 protected $entityFactory;
43 /**
44 * TODO: This variable can be deleted when row indices get preserved.
45 *
46 * @var int Number of read rows
47 */
48 protected $numReadRows = 0;
49 /** @var Row Contains the row currently processed */
50 protected $currentlyProcessedRow;
51 /** @var null|Row Buffer used to store the current row, while checking if there are more rows to read */
52 protected $rowBuffer;
53 /** @var bool Indicates whether all rows have been read */
54 protected $hasReachedEndOfFile = \false;
55 /** @var int The number of columns the sheet has (0 meaning undefined) */
56 protected $numColumns = 0;
57 /** @var bool Whether empty rows should be returned or skipped */
58 protected $shouldPreserveEmptyRows;
59 /** @var int Last row index processed (one-based) */
60 protected $lastRowIndexProcessed = 0;
61 /** @var int Row index to be processed next (one-based) */
62 protected $nextRowIndexToBeProcessed = 0;
63 /** @var int Last column index processed (zero-based) */
64 protected $lastColumnIndexProcessed = -1;
65 /**
66 * @param string $filePath Path of the XLSX file being read
67 * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
68 * @param bool $shouldPreserveEmptyRows Whether empty rows should be preserved
69 * @param XMLReader $xmlReader XML Reader
70 * @param XMLProcessor $xmlProcessor Helper to process XML files
71 * @param CellValueFormatter $cellValueFormatter Helper to format cell values
72 * @param RowManager $rowManager Manages rows
73 * @param InternalEntityFactory $entityFactory Factory to create entities
74 */
75 public function __construct($filePath, $sheetDataXMLFilePath, $shouldPreserveEmptyRows, $xmlReader, XMLProcessor $xmlProcessor, CellValueFormatter $cellValueFormatter, RowManager $rowManager, InternalEntityFactory $entityFactory)
76 {
77 $this->filePath = $filePath;
78 $this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
79 $this->shouldPreserveEmptyRows = $shouldPreserveEmptyRows;
80 $this->xmlReader = $xmlReader;
81 $this->cellValueFormatter = $cellValueFormatter;
82 $this->rowManager = $rowManager;
83 $this->entityFactory = $entityFactory;
84 // Register all callbacks to process different nodes when reading the XML file
85 $this->xmlProcessor = $xmlProcessor;
86 $this->xmlProcessor->registerCallback(self::XML_NODE_DIMENSION, XMLProcessor::NODE_TYPE_START, [$this, 'processDimensionStartingNode']);
87 $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']);
88 $this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']);
89 $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']);
90 $this->xmlProcessor->registerCallback(self::XML_NODE_WORKSHEET, XMLProcessor::NODE_TYPE_END, [$this, 'processWorksheetEndingNode']);
91 }
92 /**
93 * Rewind the Iterator to the first element.
94 * Initializes the XMLReader object that reads the associated sheet data.
95 * The XMLReader is configured to be safe from billion laughs attack.
96 *
97 * @see http://php.net/manual/en/iterator.rewind.php
98 *
99 * @throws \OpenSpout\Common\Exception\IOException If the sheet data XML cannot be read
100 */
101 #[\ReturnTypeWillChange]
102 public function rewind() : void
103 {
104 $this->xmlReader->close();
105 if (\false === $this->xmlReader->openFileInZip($this->filePath, $this->sheetDataXMLFilePath)) {
106 throw new IOException("Could not open \"{$this->sheetDataXMLFilePath}\".");
107 }
108 $this->numReadRows = 0;
109 $this->lastRowIndexProcessed = 0;
110 $this->nextRowIndexToBeProcessed = 0;
111 $this->rowBuffer = null;
112 $this->hasReachedEndOfFile = \false;
113 $this->numColumns = 0;
114 $this->next();
115 }
116 /**
117 * Checks if current position is valid.
118 *
119 * @see http://php.net/manual/en/iterator.valid.php
120 */
121 #[\ReturnTypeWillChange]
122 public function valid() : bool
123 {
124 return !$this->hasReachedEndOfFile;
125 }
126 /**
127 * Move forward to next element. Reads data describing the next unprocessed row.
128 *
129 * @see http://php.net/manual/en/iterator.next.php
130 *
131 * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
132 * @throws \OpenSpout\Common\Exception\IOException If unable to read the sheet data XML
133 */
134 #[\ReturnTypeWillChange]
135 public function next() : void
136 {
137 ++$this->nextRowIndexToBeProcessed;
138 if ($this->doesNeedDataForNextRowToBeProcessed()) {
139 $this->readDataForNextRow();
140 }
141 }
142 /**
143 * Return the current element, either an empty row or from the buffer.
144 *
145 * @see http://php.net/manual/en/iterator.current.php
146 */
147 #[\ReturnTypeWillChange]
148 public function current() : ?Row
149 {
150 $rowToBeProcessed = $this->rowBuffer;
151 if ($this->shouldPreserveEmptyRows) {
152 // when we need to preserve empty rows, we will either return
153 // an empty row or the last row read. This depends whether the
154 // index of last row that was read matches the index of the last
155 // row whose value should be returned.
156 if ($this->lastRowIndexProcessed !== $this->nextRowIndexToBeProcessed) {
157 // return empty row if mismatch between last processed row
158 // and the row that needs to be returned
159 $rowToBeProcessed = $this->entityFactory->createRow();
160 }
161 }
162 return $rowToBeProcessed;
163 }
164 /**
165 * Return the key of the current element. Here, the row index.
166 *
167 * @see http://php.net/manual/en/iterator.key.php
168 */
169 #[\ReturnTypeWillChange]
170 public function key() : int
171 {
172 // TODO: This should return $this->nextRowIndexToBeProcessed
173 // but to avoid a breaking change, the return value for
174 // this function has been kept as the number of rows read.
175 return $this->shouldPreserveEmptyRows ? $this->nextRowIndexToBeProcessed : $this->numReadRows;
176 }
177 /**
178 * Cleans up what was created to iterate over the object.
179 */
180 #[\ReturnTypeWillChange]
181 public function end() : void
182 {
183 $this->xmlReader->close();
184 }
185 /**
186 * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
187 *
188 * @return string path of the XML file containing the sheet data,
189 * without the leading slash
190 */
191 protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath)
192 {
193 return \ltrim($sheetDataXMLFilePath, '/');
194 }
195 /**
196 * Returns whether we need data for the next row to be processed.
197 * We don't need to read data if:
198 * we have already read at least one row
199 * AND
200 * we need to preserve empty rows
201 * AND
202 * the last row that was read is not the row that need to be processed
203 * (i.e. if we need to return empty rows).
204 *
205 * @return bool whether we need data for the next row to be processed
206 */
207 protected function doesNeedDataForNextRowToBeProcessed()
208 {
209 $hasReadAtLeastOneRow = 0 !== $this->lastRowIndexProcessed;
210 return !$hasReadAtLeastOneRow || !$this->shouldPreserveEmptyRows || $this->lastRowIndexProcessed < $this->nextRowIndexToBeProcessed;
211 }
212 /**
213 * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
214 * @throws \OpenSpout\Common\Exception\IOException If unable to read the sheet data XML
215 */
216 protected function readDataForNextRow()
217 {
218 $this->currentlyProcessedRow = $this->entityFactory->createRow();
219 try {
220 $this->xmlProcessor->readUntilStopped();
221 } catch (XMLProcessingException $exception) {
222 throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]");
223 }
224 $this->rowBuffer = $this->currentlyProcessedRow;
225 }
226 /**
227 * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<dimension>" starting node
228 *
229 * @return int A return code that indicates what action should the processor take next
230 */
231 protected function processDimensionStartingNode($xmlReader)
232 {
233 // Read dimensions of the sheet
234 $dimensionRef = $xmlReader->getAttribute(self::XML_ATTRIBUTE_REF);
235 // returns 'A1:M13' for instance (or 'A1' for empty sheet)
236 if (\preg_match('/[A-Z]+\\d+:([A-Z]+\\d+)/', $dimensionRef, $matches)) {
237 $this->numColumns = CellHelper::getColumnIndexFromCellIndex($matches[1]) + 1;
238 }
239 return XMLProcessor::PROCESSING_CONTINUE;
240 }
241 /**
242 * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<row>" starting node
243 *
244 * @return int A return code that indicates what action should the processor take next
245 */
246 protected function processRowStartingNode($xmlReader)
247 {
248 // Reset index of the last processed column
249 $this->lastColumnIndexProcessed = -1;
250 // Mark the last processed row as the one currently being read
251 $this->lastRowIndexProcessed = $this->getRowIndex($xmlReader);
252 // Read spans info if present
253 $numberOfColumnsForRow = $this->numColumns;
254 $spans = $xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS);
255 // returns '1:5' for instance
256 if ($spans) {
257 [, $numberOfColumnsForRow] = \explode(':', $spans);
258 $numberOfColumnsForRow = (int) $numberOfColumnsForRow;
259 }
260 $cells = \array_fill(0, $numberOfColumnsForRow, $this->entityFactory->createCell(''));
261 $this->currentlyProcessedRow->setCells($cells);
262 return XMLProcessor::PROCESSING_CONTINUE;
263 }
264 /**
265 * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<cell>" starting node
266 *
267 * @return int A return code that indicates what action should the processor take next
268 */
269 protected function processCellStartingNode($xmlReader)
270 {
271 $currentColumnIndex = $this->getColumnIndex($xmlReader);
272 // NOTE: expand() will automatically decode all XML entities of the child nodes
273 /** @var \DOMElement $node */
274 $node = $xmlReader->expand();
275 $cell = $this->getCell($node);
276 $this->currentlyProcessedRow->setCellAtIndex($cell, $currentColumnIndex);
277 $this->lastColumnIndexProcessed = $currentColumnIndex;
278 return XMLProcessor::PROCESSING_CONTINUE;
279 }
280 /**
281 * @return int A return code that indicates what action should the processor take next
282 */
283 protected function processRowEndingNode()
284 {
285 // if the fetched row is empty and we don't want to preserve it..,
286 if (!$this->shouldPreserveEmptyRows && $this->rowManager->isEmpty($this->currentlyProcessedRow)) {
287 // ... skip it
288 return XMLProcessor::PROCESSING_CONTINUE;
289 }
290 ++$this->numReadRows;
291 // If needed, we fill the empty cells
292 if (0 === $this->numColumns) {
293 $this->currentlyProcessedRow = $this->rowManager->fillMissingIndexesWithEmptyCells($this->currentlyProcessedRow);
294 }
295 // at this point, we have all the data we need for the row
296 // so that we can populate the buffer
297 return XMLProcessor::PROCESSING_STOP;
298 }
299 /**
300 * @return int A return code that indicates what action should the processor take next
301 */
302 protected function processWorksheetEndingNode()
303 {
304 // The closing "</worksheet>" marks the end of the file
305 $this->hasReachedEndOfFile = \true;
306 return XMLProcessor::PROCESSING_STOP;
307 }
308 /**
309 * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<row>" node
310 *
311 * @throws \OpenSpout\Common\Exception\InvalidArgumentException When the given cell index is invalid
312 *
313 * @return int Row index
314 */
315 protected function getRowIndex($xmlReader)
316 {
317 // Get "r" attribute if present (from something like <row r="3"...>
318 $currentRowIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ROW_INDEX);
319 return null !== $currentRowIndex ? (int) $currentRowIndex : $this->lastRowIndexProcessed + 1;
320 }
321 /**
322 * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<c>" node
323 *
324 * @throws \OpenSpout\Common\Exception\InvalidArgumentException When the given cell index is invalid
325 *
326 * @return int Column index
327 */
328 protected function getColumnIndex($xmlReader)
329 {
330 // Get "r" attribute if present (from something like <c r="A1"...>
331 $currentCellIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX);
332 return null !== $currentCellIndex ? CellHelper::getColumnIndexFromCellIndex($currentCellIndex) : $this->lastColumnIndexProcessed + 1;
333 }
334 /**
335 * Returns the cell with (unescaped) correctly marshalled, cell value associated to the given XML node.
336 *
337 * @param \DOMElement $node
338 *
339 * @return Cell The cell set with the associated with the cell
340 */
341 protected function getCell($node)
342 {
343 try {
344 $cellValue = $this->cellValueFormatter->extractAndFormatNodeValue($node);
345 $cell = $this->entityFactory->createCell($cellValue);
346 } catch (InvalidValueException $exception) {
347 $cell = $this->entityFactory->createCell($exception->getInvalidValue());
348 $cell->setType(Cell::TYPE_ERROR);
349 }
350 return $cell;
351 }
352 }
353