PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 3.6.51
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v3.6.51
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Services / Spout / Reader / XLSX / RowIterator.php

RowIterator.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 3.6.51, at app/Services/Spout/Reader/XLSX/RowIterator.php

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