Completed
Pull Request — master (#235)
by
unknown
03:06
created

RowIterator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 6
ccs 5
cts 5
cp 1
rs 9.4285
cc 1
eloc 4
nc 1
nop 2
crap 1
1
<?php
2
3
namespace Box\Spout\Reader\ODS;
4
5
use Box\Spout\Common\Exception\IOException;
6
use Box\Spout\Reader\Exception\IteratorNotRewindableException;
7
use Box\Spout\Reader\Exception\XMLProcessingException;
8
use Box\Spout\Reader\IteratorInterface;
9
use Box\Spout\Reader\ODS\Helper\CellValueFormatter;
10
use Box\Spout\Reader\Wrapper\XMLReader;
11
use Box\Spout\Reader\ReaderOptions;
12
13
/**
14
 * Class RowIterator
15
 *
16
 * @package Box\Spout\Reader\ODS
17
 */
18
class RowIterator implements IteratorInterface
19
{
20
    /** Definition of XML nodes names used to parse data */
21
    const XML_NODE_TABLE = 'table:table';
22
    const XML_NODE_ROW = 'table:table-row';
23
    const XML_NODE_CELL = 'table:table-cell';
24
    const MAX_COLUMNS_EXCEL = 16384;
25
    const MAX_ROWS_EXCEL = 1048576;
26
27
    /** Definition of XML attribute used to parse data */
28
    const XML_ATTRIBUTE_NUM_COLUMNS_REPEATED = 'table:number-columns-repeated';
29
30
    /** Definition of XML attribute used to parse data */
31
    const XML_ATTRIBUTE_NUM_ROWS_REPEATED = 'table:number-rows-repeated';
32
33
    /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
34
    protected $xmlReader;
35
36
    /** @var Helper\CellValueFormatter Helper to format cell values */
37
    protected $cellValueFormatter;
38
39
    /** @var bool Whether the iterator has already been rewound once */
40
    protected $hasAlreadyBeenRewound = false;
41
42
    /** @var int Key for iterator */
43
    protected $rowIndex = 0;
44
45
    /** @var array Buffer used to store the row data, while checking if there are more rows to read */
46
    protected $rowDataBuffer = [];
47
48
    /** @var bool Indicates whether all rows have been read */
49
    protected $hasReachedEndOfFile = false;
50
51
    /** @var \Box\Spout\Reader\ReaderOptions */
52
    protected $readerOptions;
53
54
    /**
55
     * @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
56
     * @param \Box\Spout\Reader\ReaderOptions $readerOptions
57
     */
58 90
    public function __construct($xmlReader, ReaderOptions $readerOptions)
59
    {
60 90
        $this->xmlReader = $xmlReader;
61 90
        $this->readerOptions = $readerOptions;
62 90
        $this->cellValueFormatter = new CellValueFormatter($readerOptions->shouldFormatDates());
63 90
    }
64
65
    /**
66
     * Rewind the Iterator to the first element.
67
     * NOTE: It can only be done once, as it is not possible to read an XML file backwards.
68
     * @link http://php.net/manual/en/iterator.rewind.php
69
     *
70
     * @return void
71
     * @throws \Box\Spout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once
72
     */
73 90
    public function rewind()
74
    {
75
        // Because sheet and row data is located in the file, we can't rewind both the
76
        // sheet iterator and the row iterator, as XML file cannot be read backwards.
77
        // Therefore, rewinding the row iterator has been disabled.
78 90
        if ($this->hasAlreadyBeenRewound) {
79 3
            throw new IteratorNotRewindableException();
80
        }
81
82 90
        $this->hasAlreadyBeenRewound = true;
83 90
        $this->rowIndex = 0;
84 90
        $this->rowDataBuffer = [];
85 90
        $this->hasReachedEndOfFile = false;
86
87 90
        $this->next();
88 90
    }
89
90
    /**
91
     * Checks if current position is valid
92
     * @link http://php.net/manual/en/iterator.valid.php
93
     *
94
     * @return boolean
95
     */
96 90
    public function valid()
97
    {
98 90
        return (!$this->hasReachedEndOfFile);
99
    }
100
101
    /**
102
     * Move forward to next element. Empty rows can be skipped.
103
     * @link http://php.net/manual/en/iterator.next.php
104
     *
105
     * @return void
106
     * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
107
     * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
108
     */
109 90
    public function next()
110
    {
111 90
        $prevRow = null;
0 ignored issues
show
Unused Code introduced by
$prevRow is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
112
113 90
        if (count($this->rowDataBuffer) > 1) {
114 6
            array_shift($this->rowDataBuffer);
115 6
            $this->rowIndex++;
116
117 6
            return;
118
        } else {
119 90
            $prevRow = $this->current();
120 90
            $this->rowDataBuffer = [];
121
        }
122
123 90
        $rowData = [];
124 90
        $cellValue = null;
125 90
        $numRowsRepeated = 0;
126 90
        $numColumnsRepeated = 1;
127 90
        $numCellsRead = 0;
128 90
        $hasAlreadyReadOneCell = false;
129
130
        try {
131 90
            while ($this->xmlReader->read()) {
132 90
                if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_ROW)) {
133
                    // Start of a row description
134 90
                    $this->rowIndex++;
135
136 90
                    $numRowsRepeated = $this->getNumRowsRepeatedForCurrentNode();
137
138 90
                } elseif ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL)) {
139
                    // Start of a cell description
140 90
                    $currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode();
141
142 90
                    $node = $this->xmlReader->expand();
143 90
                    $currentCellValue = $this->getCellValue($node);
144
145
                    // process cell N only after having read cell N+1 (see below why)
146 90
                    if ($hasAlreadyReadOneCell) {
147 72
                        for ($i = 0; $i < $numColumnsRepeated; $i++) {
148 72
                            $rowData[] = $cellValue;
149 72
                        }
150 72
                    }
151
152 90
                    $cellValue = $currentCellValue;
153 90
                    $numColumnsRepeated = $currentNumColumnsRepeated;
154
155 90
                    $numCellsRead++;
156 90
                    $hasAlreadyReadOneCell = true;
157
158 90
                } elseif ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) {
159
                    // End of the row description
160 90
                    $isEmptyRow = ($numCellsRead <= 1 && $this->isEmptyCellValue($cellValue));
161
162 90
                    if (!$isEmptyRow) {
163
                        // Only add the value if the last read cell is not a trailing empty cell repeater in Excel.
164
                        // The current count of read columns is determined by counting the values in $rowData.
165
                        // This is to avoid creating a lot of empty cells, as Excel adds a last empty "<table:table-cell>"
166
                        // with a number-columns-repeated value equals to the number of (supported columns - used columns).
167
                        // In Excel, the number of supported columns is 16384, but we don't want to returns rows with
168
                        // always 16384 cells.
169 87
                        if ((count($rowData) + $numColumnsRepeated) !== self::MAX_COLUMNS_EXCEL) {
170 72
                            for ($i = 0; $i < $numColumnsRepeated; $i++) {
171 72
                                $rowData[] = $cellValue;
172 72
                            }
173 72
                        }
174 90
                    } elseif ($this->readerOptions->shouldPreserveEmptyRows()) {
175
                        // Take number of cells from the previously read line.
176 18
                        $rowData = empty($prevRow) ? [] : array_fill(0, count($prevRow), '');
177 18
                    } else {
178 18
                        return $this->next();
179
                    }
180
181
                    // see above, now check number of rows...
182 87
                    if ($this->rowIndex - 1 + $numRowsRepeated >= self::MAX_ROWS_EXCEL) {
183 9
                        $numRowsRepeated = 0;
184 9
                        $this->hasReachedEndOfFile = true;
185 9
                    }
186 87
                    break;
187
188 90
                } elseif ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_TABLE)) {
189
                    // The closing "</table:table>" marks the end of the file
190 78
                    $this->hasReachedEndOfFile = true;
191 78
                    break;
192
                }
193 90
            }
194
195 90
        } catch (XMLProcessingException $exception) {
196
            throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]");
197
        }
198
199 90
        for ($i = 0; $i < $numRowsRepeated; ++$i) {
200 87
            $this->rowDataBuffer[] = $rowData;
201 87
        }
202 90
    }
203
204
    /**
205
     * @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing
206
     */
207 90
    protected function getNumColumnsRepeatedForCurrentNode()
208
    {
209 90
        $numColumnsRepeated = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED);
210 90
        return ($numColumnsRepeated !== null) ? intval($numColumnsRepeated) : 1;
211
    }
212
213
    /**
214
     * @return int The value of "table:number-rows-repeated" attribute of the current node, or 1 if attribute missing
215
     */
216 90
    protected function getNumRowsRepeatedForCurrentNode()
217
    {
218 90
        $numRowsRepeated = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_ROWS_REPEATED);
219 90
        return ($numRowsRepeated !== null) ? intval($numRowsRepeated) : 1;
220
    }
221
222
    /**
223
     * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
224
     *
225
     * @param \DOMNode $node
226
     * @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error
227
     */
228 90
    protected function getCellValue($node)
229
    {
230 90
        return $this->cellValueFormatter->extractAndFormatNodeValue($node);
231
    }
232
233
    /**
234
     * empty() replacement that honours 0 as a valid value
235
     *
236
     * @param string|int|float|bool|\DateTime|\DateInterval|null $value The cell value
237
     * @return bool
238
     */
239 48
    protected function isEmptyCellValue($value)
240
    {
241 48
        return (!isset($value) || trim($value) === '');
242
    }
243
244
    /**
245
     * Return the current element, from the buffer.
246
     * @link http://php.net/manual/en/iterator.current.php
247
     *
248
     * @return array|null
249
     */
250 90
    public function current()
251
    {
252 90
        return isset($this->rowDataBuffer[0]) ? $this->rowDataBuffer[0] : null;
253
    }
254
255
    /**
256
     * Return the key of the current element
257
     * @link http://php.net/manual/en/iterator.key.php
258
     *
259
     * @return int
260
     */
261 12
    public function key()
262
    {
263 12
        return $this->rowIndex;
264
    }
265
266
    /**
267
     * Cleans up what was created to iterate over the object.
268
     *
269
     * @return void
270
     */
271
    public function end()
272
    {
273
        $this->xmlReader->close();
274
    }
275
}
276