PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.1
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 trunk All 48 releases
fluent-cart / vendor / openspout / openspout / src / Reader / ODS / RowIterator.php

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

321 lines 14.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\ODS;
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\Common\Manager\OptionsManagerInterface;
9 use FluentCart\OpenSpout\Reader\Common\Entity\Options;
10 use FluentCart\OpenSpout\Reader\Common\Manager\RowManager;
11 use FluentCart\OpenSpout\Reader\Common\XMLProcessor;
12 use FluentCart\OpenSpout\Reader\Exception\InvalidValueException;
13 use FluentCart\OpenSpout\Reader\Exception\IteratorNotRewindableException;
14 use FluentCart\OpenSpout\Reader\Exception\XMLProcessingException;
15 use FluentCart\OpenSpout\Reader\IteratorInterface;
16 use FluentCart\OpenSpout\Reader\ODS\Creator\InternalEntityFactory;
17 use FluentCart\OpenSpout\Reader\ODS\Helper\CellValueFormatter;
18 use FluentCart\OpenSpout\Reader\Wrapper\XMLReader;
19 class RowIterator implements IteratorInterface
20 {
21 /** Definition of XML nodes names used to parse data */
22 public const XML_NODE_TABLE = 'table:table';
23 public const XML_NODE_ROW = 'table:table-row';
24 public const XML_NODE_CELL = 'table:table-cell';
25 public const MAX_COLUMNS_EXCEL = 16384;
26 /** Definition of XML attribute used to parse data */
27 public const XML_ATTRIBUTE_NUM_ROWS_REPEATED = 'table:number-rows-repeated';
28 public const XML_ATTRIBUTE_NUM_COLUMNS_REPEATED = 'table:number-columns-repeated';
29 /** @var \OpenSpout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
30 protected $xmlReader;
31 /** @var \OpenSpout\Reader\Common\XMLProcessor Helper Object to process XML nodes */
32 protected $xmlProcessor;
33 /** @var bool Whether empty rows should be returned or skipped */
34 protected $shouldPreserveEmptyRows;
35 /** @var Helper\CellValueFormatter Helper to format cell values */
36 protected $cellValueFormatter;
37 /** @var RowManager Manages rows */
38 protected $rowManager;
39 /** @var InternalEntityFactory Factory to create entities */
40 protected $entityFactory;
41 /** @var bool Whether the iterator has already been rewound once */
42 protected $hasAlreadyBeenRewound = \false;
43 /** @var Row The currently processed row */
44 protected $currentlyProcessedRow;
45 /** @var null|Row Buffer used to store the current row, while checking if there are more rows to read */
46 protected $rowBuffer;
47 /** @var bool Indicates whether all rows have been read */
48 protected $hasReachedEndOfFile = \false;
49 /** @var int Last row index processed (one-based) */
50 protected $lastRowIndexProcessed = 0;
51 /** @var int Row index to be processed next (one-based) */
52 protected $nextRowIndexToBeProcessed = 1;
53 /** @var null|Cell Last processed cell (because when reading cell at column N+1, cell N is processed) */
54 protected $lastProcessedCell;
55 /** @var int Number of times the last processed row should be repeated */
56 protected $numRowsRepeated = 1;
57 /** @var int Number of times the last cell value should be copied to the cells on its right */
58 protected $numColumnsRepeated = 1;
59 /** @var bool Whether at least one cell has been read for the row currently being processed */
60 protected $hasAlreadyReadOneCellInCurrentRow = \false;
61 /**
62 * @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
63 * @param OptionsManagerInterface $optionsManager Reader's options manager
64 * @param CellValueFormatter $cellValueFormatter Helper to format cell values
65 * @param XMLProcessor $xmlProcessor Helper to process XML files
66 * @param RowManager $rowManager Manages rows
67 * @param InternalEntityFactory $entityFactory Factory to create entities
68 */
69 public function __construct(XMLReader $xmlReader, OptionsManagerInterface $optionsManager, CellValueFormatter $cellValueFormatter, XMLProcessor $xmlProcessor, RowManager $rowManager, InternalEntityFactory $entityFactory)
70 {
71 $this->xmlReader = $xmlReader;
72 $this->shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS);
73 $this->cellValueFormatter = $cellValueFormatter;
74 $this->entityFactory = $entityFactory;
75 $this->rowManager = $rowManager;
76 // Register all callbacks to process different nodes when reading the XML file
77 $this->xmlProcessor = $xmlProcessor;
78 $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']);
79 $this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']);
80 $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']);
81 $this->xmlProcessor->registerCallback(self::XML_NODE_TABLE, XMLProcessor::NODE_TYPE_END, [$this, 'processTableEndingNode']);
82 }
83 /**
84 * Rewind the Iterator to the first element.
85 * NOTE: It can only be done once, as it is not possible to read an XML file backwards.
86 *
87 * @see http://php.net/manual/en/iterator.rewind.php
88 *
89 * @throws \OpenSpout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once
90 */
91 #[\ReturnTypeWillChange]
92 public function rewind() : void
93 {
94 // Because sheet and row data is located in the file, we can't rewind both the
95 // sheet iterator and the row iterator, as XML file cannot be read backwards.
96 // Therefore, rewinding the row iterator has been disabled.
97 if ($this->hasAlreadyBeenRewound) {
98 throw new IteratorNotRewindableException();
99 }
100 $this->hasAlreadyBeenRewound = \true;
101 $this->lastRowIndexProcessed = 0;
102 $this->nextRowIndexToBeProcessed = 1;
103 $this->rowBuffer = null;
104 $this->hasReachedEndOfFile = \false;
105 $this->next();
106 }
107 /**
108 * Checks if current position is valid.
109 *
110 * @see http://php.net/manual/en/iterator.valid.php
111 */
112 #[\ReturnTypeWillChange]
113 public function valid() : bool
114 {
115 return !$this->hasReachedEndOfFile;
116 }
117 /**
118 * Move forward to next element. Empty rows will be skipped.
119 *
120 * @see http://php.net/manual/en/iterator.next.php
121 *
122 * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
123 * @throws \OpenSpout\Common\Exception\IOException If unable to read the sheet data XML
124 */
125 #[\ReturnTypeWillChange]
126 public function next() : void
127 {
128 if ($this->doesNeedDataForNextRowToBeProcessed()) {
129 $this->readDataForNextRow();
130 }
131 ++$this->lastRowIndexProcessed;
132 }
133 /**
134 * Return the current element, from the buffer.
135 *
136 * @see http://php.net/manual/en/iterator.current.php
137 */
138 #[\ReturnTypeWillChange]
139 public function current() : Row
140 {
141 return $this->rowBuffer;
142 }
143 /**
144 * Return the key of the current element.
145 *
146 * @see http://php.net/manual/en/iterator.key.php
147 */
148 #[\ReturnTypeWillChange]
149 public function key() : int
150 {
151 return $this->lastRowIndexProcessed;
152 }
153 /**
154 * Cleans up what was created to iterate over the object.
155 */
156 #[\ReturnTypeWillChange]
157 public function end() : void
158 {
159 $this->xmlReader->close();
160 }
161 /**
162 * Returns whether we need data for the next row to be processed.
163 * We DO need to read data if:
164 * - we have not read any rows yet
165 * OR
166 * - the next row to be processed immediately follows the last read row.
167 *
168 * @return bool whether we need data for the next row to be processed
169 */
170 protected function doesNeedDataForNextRowToBeProcessed()
171 {
172 $hasReadAtLeastOneRow = 0 !== $this->lastRowIndexProcessed;
173 return !$hasReadAtLeastOneRow || $this->lastRowIndexProcessed === $this->nextRowIndexToBeProcessed - 1;
174 }
175 /**
176 * @throws \OpenSpout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
177 * @throws \OpenSpout\Common\Exception\IOException If unable to read the sheet data XML
178 */
179 protected function readDataForNextRow()
180 {
181 $this->currentlyProcessedRow = $this->entityFactory->createRow();
182 try {
183 $this->xmlProcessor->readUntilStopped();
184 } catch (XMLProcessingException $exception) {
185 throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]");
186 }
187 $this->rowBuffer = $this->currentlyProcessedRow;
188 }
189 /**
190 * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-row>" starting node
191 *
192 * @return int A return code that indicates what action should the processor take next
193 */
194 protected function processRowStartingNode($xmlReader)
195 {
196 // Reset data from current row
197 $this->hasAlreadyReadOneCellInCurrentRow = \false;
198 $this->lastProcessedCell = null;
199 $this->numColumnsRepeated = 1;
200 $this->numRowsRepeated = $this->getNumRowsRepeatedForCurrentNode($xmlReader);
201 return XMLProcessor::PROCESSING_CONTINUE;
202 }
203 /**
204 * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-cell>" starting node
205 *
206 * @return int A return code that indicates what action should the processor take next
207 */
208 protected function processCellStartingNode($xmlReader)
209 {
210 $currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode($xmlReader);
211 // NOTE: expand() will automatically decode all XML entities of the child nodes
212 /** @var \DOMElement $node */
213 $node = $xmlReader->expand();
214 $currentCell = $this->getCell($node);
215 // process cell N only after having read cell N+1 (see below why)
216 if ($this->hasAlreadyReadOneCellInCurrentRow) {
217 for ($i = 0; $i < $this->numColumnsRepeated; ++$i) {
218 $this->currentlyProcessedRow->addCell($this->lastProcessedCell);
219 }
220 }
221 $this->hasAlreadyReadOneCellInCurrentRow = \true;
222 $this->lastProcessedCell = $currentCell;
223 $this->numColumnsRepeated = $currentNumColumnsRepeated;
224 return XMLProcessor::PROCESSING_CONTINUE;
225 }
226 /**
227 * @return int A return code that indicates what action should the processor take next
228 */
229 protected function processRowEndingNode()
230 {
231 $isEmptyRow = $this->isEmptyRow($this->currentlyProcessedRow, $this->lastProcessedCell);
232 // if the fetched row is empty and we don't want to preserve it...
233 if (!$this->shouldPreserveEmptyRows && $isEmptyRow) {
234 // ... skip it
235 return XMLProcessor::PROCESSING_CONTINUE;
236 }
237 // if the row is empty, we don't want to return more than one cell
238 $actualNumColumnsRepeated = !$isEmptyRow ? $this->numColumnsRepeated : 1;
239 $numCellsInCurrentlyProcessedRow = $this->currentlyProcessedRow->getNumCells();
240 // Only add the value if the last read cell is not a trailing empty cell repeater in Excel.
241 // The current count of read columns is determined by counting the values in "$this->currentlyProcessedRowData".
242 // This is to avoid creating a lot of empty cells, as Excel adds a last empty "<table:table-cell>"
243 // with a number-columns-repeated value equals to the number of (supported columns - used columns).
244 // In Excel, the number of supported columns is 16384, but we don't want to returns rows with
245 // always 16384 cells.
246 if ($numCellsInCurrentlyProcessedRow + $actualNumColumnsRepeated !== self::MAX_COLUMNS_EXCEL) {
247 for ($i = 0; $i < $actualNumColumnsRepeated; ++$i) {
248 $this->currentlyProcessedRow->addCell($this->lastProcessedCell);
249 }
250 }
251 // If we are processing row N and the row is repeated M times,
252 // then the next row to be processed will be row (N+M).
253 $this->nextRowIndexToBeProcessed += $this->numRowsRepeated;
254 // at this point, we have all the data we need for the row
255 // so that we can populate the buffer
256 return XMLProcessor::PROCESSING_STOP;
257 }
258 /**
259 * @return int A return code that indicates what action should the processor take next
260 */
261 protected function processTableEndingNode()
262 {
263 // The closing "</table:table>" marks the end of the file
264 $this->hasReachedEndOfFile = \true;
265 return XMLProcessor::PROCESSING_STOP;
266 }
267 /**
268 * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-row>" starting node
269 *
270 * @return int The value of "table:number-rows-repeated" attribute of the current node, or 1 if attribute missing
271 */
272 protected function getNumRowsRepeatedForCurrentNode($xmlReader)
273 {
274 $numRowsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_ROWS_REPEATED);
275 return null !== $numRowsRepeated ? (int) $numRowsRepeated : 1;
276 }
277 /**
278 * @param \OpenSpout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-cell>" starting node
279 *
280 * @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing
281 */
282 protected function getNumColumnsRepeatedForCurrentNode($xmlReader)
283 {
284 $numColumnsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED);
285 return null !== $numColumnsRepeated ? (int) $numColumnsRepeated : 1;
286 }
287 /**
288 * Returns the cell with (unescaped) correctly marshalled, cell value associated to the given XML node.
289 *
290 * @param \DOMElement $node
291 *
292 * @return Cell The cell set with the associated with the cell
293 */
294 protected function getCell($node)
295 {
296 try {
297 $cellValue = $this->cellValueFormatter->extractAndFormatNodeValue($node);
298 $cell = $this->entityFactory->createCell($cellValue);
299 } catch (InvalidValueException $exception) {
300 $cell = $this->entityFactory->createCell($exception->getInvalidValue());
301 $cell->setType(Cell::TYPE_ERROR);
302 }
303 return $cell;
304 }
305 /**
306 * After finishing processing each cell, a row is considered empty if it contains
307 * no cells or if the last read cell is empty.
308 * After finishing processing each cell, the last read cell is not part of the
309 * row data yet (as we still need to apply the "num-columns-repeated" attribute).
310 *
311 * @param Row $currentRow
312 * @param null|Cell $lastReadCell The last read cell
313 *
314 * @return bool Whether the row is empty
315 */
316 protected function isEmptyRow($currentRow, $lastReadCell)
317 {
318 return $this->rowManager->isEmpty($currentRow) && (!isset($lastReadCell) || $lastReadCell->isEmpty());
319 }
320 }
321