Failed Conditions
Push — support_missing_unique_count_a... ( 44c282...f73db4 )
by Adrien
31:35 queued 28:53
created

RowIterator::getCellIndex()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.6666
cc 2
eloc 5
nc 2
nop 1
crap 2
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
13
/**
14
 * Class RowIterator
15
 *
16
 * @package Box\Spout\Reader\XLSX
17
 */
18
class RowIterator implements IteratorInterface
19
{
20
    /** Definition of XML nodes names used to parse data */
21
    const XML_NODE_DIMENSION = 'dimension';
22
    const XML_NODE_WORKSHEET = 'worksheet';
23
    const XML_NODE_ROW = 'row';
24
    const XML_NODE_CELL = 'c';
25
26
    /** Definition of XML attributes used to parse data */
27
    const XML_ATTRIBUTE_REF = 'ref';
28
    const XML_ATTRIBUTE_SPANS = 'spans';
29
    const XML_ATTRIBUTE_CELL_INDEX = 'r';
30
31
    /** @var string Path of the XLSX file being read */
32
    protected $filePath;
33
34
    /** @var string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml */
35
    protected $sheetDataXMLFilePath;
36
37
    /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
38
    protected $xmlReader;
39
40
    /** @var Helper\CellValueFormatter Helper to format cell values */
41
    protected $cellValueFormatter;
42
43
    /** @var Helper\StyleHelper $styleHelper Helper to work with styles */
44
    protected $styleHelper;
45
46
    /** @var int Number of read rows */
47
    protected $numReadRows = 0;
48
49
    /** @var array|null Buffer used to store the row data, while checking if there are more rows to read */
50
    protected $rowDataBuffer = null;
51
52
    /** @var bool Indicates whether all rows have been read */
53
    protected $hasReachedEndOfFile = false;
54
55
    /** @var int The number of columns the sheet has (0 meaning undefined) */
56
    protected $numColumns = 0;
57
58
    /** @var int Last column index processed (zero-based) */
59
    protected $lastColumnIndexProcessed = -1;
60
61
    /**
62
     * @param string $filePath Path of the XLSX file being read
63
     * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
64
     * @param Helper\SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
65
     * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
66
     */
67 90
    public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $shouldFormatDates)
68
    {
69 90
        $this->filePath = $filePath;
70 90
        $this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
71
72 90
        $this->xmlReader = new XMLReader();
73
74 90
        $this->styleHelper = new StyleHelper($filePath);
75 90
        $this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper, $shouldFormatDates);
76 90
    }
77
78
    /**
79
     * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
80
     * @return string Path of the XML file containing the sheet data,
81
     *                without the leading slash.
82
     */
83 90
    protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath)
84
    {
85 90
        return ltrim($sheetDataXMLFilePath, '/');
86
    }
87
88
    /**
89
     * Rewind the Iterator to the first element.
90
     * Initializes the XMLReader object that reads the associated sheet data.
91
     * The XMLReader is configured to be safe from billion laughs attack.
92
     * @link http://php.net/manual/en/iterator.rewind.php
93
     *
94
     * @return void
95
     * @throws \Box\Spout\Common\Exception\IOException If the sheet data XML cannot be read
96
     */
97 87
    public function rewind()
98
    {
99 87
        $this->xmlReader->close();
100
101 87
        $sheetDataFilePath = 'zip://' . $this->filePath . '#' . $this->sheetDataXMLFilePath;
102 87
        if ($this->xmlReader->open($sheetDataFilePath) === false) {
103 3
            throw new IOException("Could not open \"{$this->sheetDataXMLFilePath}\".");
104
        }
105
106 84
        $this->numReadRows = 0;
107 84
        $this->rowDataBuffer = null;
108 84
        $this->hasReachedEndOfFile = false;
109 84
        $this->numColumns = 0;
110
111 84
        $this->next();
112 84
    }
113
114
    /**
115
     * Checks if current position is valid
116
     * @link http://php.net/manual/en/iterator.valid.php
117
     *
118
     * @return boolean
119
     */
120 84
    public function valid()
121
    {
122 84
        return (!$this->hasReachedEndOfFile);
123
    }
124
125
    /**
126
     * Move forward to next element. Empty rows will be skipped.
127
     * @link http://php.net/manual/en/iterator.next.php
128
     *
129
     * @return void
130
     * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
131
     * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
132
     */
133 84
    public function next()
134
    {
135 84
        $rowData = [];
136
137
        try {
138 84
            while ($this->xmlReader->read()) {
139 84
                if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_DIMENSION)) {
140
                    // Read dimensions of the sheet
141 39
                    $dimensionRef = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet)
142 39
                    if (preg_match('/[A-Z\d]+:([A-Z\d]+)/', $dimensionRef, $matches)) {
143 30
                        $lastCellIndex = $matches[1];
144 30
                        $this->numColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
145 30
                    }
146
147 84
                } else if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_ROW)) {
148
                    // Start of the row description
149
150
                    // Reset index of the last processed column
151 81
                    $this->lastColumnIndexProcessed = -1;
152
153
                    // Read spans info if present
154 81
                    $numberOfColumnsForRow = $this->numColumns;
155 81
                    $spans = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance
156 81
                    if ($spans) {
157 27
                        list(, $numberOfColumnsForRow) = explode(':', $spans);
158 27
                        $numberOfColumnsForRow = intval($numberOfColumnsForRow);
159 27
                    }
160 81
                    $rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
161
162 84
                } else if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL)) {
163
                    // Start of a cell description
164 81
                    $currentColumnIndex = $this->getCellIndex($this->xmlReader);
165
166 81
                    $node = $this->xmlReader->expand();
167 81
                    $rowData[$currentColumnIndex] = $this->getCellValue($node);
168
169 81
                    $this->lastColumnIndexProcessed = $currentColumnIndex;
170
171 84
                } else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) {
172
                    // End of the row description
173
                    // If needed, we fill the empty cells
174 81
                    $rowData = ($this->numColumns !== 0) ? $rowData : CellHelper::fillMissingArrayIndexes($rowData);
175 81
                    $this->numReadRows++;
176 81
                    break;
177
178 84
                } else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_WORKSHEET)) {
179
                    // The closing "</worksheet>" marks the end of the file
180 81
                    $this->hasReachedEndOfFile = true;
181 81
                    break;
182
                }
183 84
            }
184
185 84
        } catch (XMLProcessingException $exception) {
186
            throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]");
187
        }
188
189 84
        $this->rowDataBuffer = $rowData;
190 84
    }
191
192
    /**
193
     * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<c>" tag
194
     * @return int
195
     * @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
196
     */
197 81
    protected function getCellIndex($xmlReader)
198
    {
199
        // Get "r" attribute if present (from something like <c r="A1"...>
200 81
        $currentCellIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX);
201
202 81
        return ($currentCellIndex !== null) ?
203 81
                CellHelper::getColumnIndexFromCellIndex($currentCellIndex) :
204 81
                $this->lastColumnIndexProcessed + 1;
205
    }
206
207
    /**
208
     * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
209
     *
210
     * @param \DOMNode $node
211
     * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error)
212
     */
213 81
    protected function getCellValue($node)
214
    {
215 81
        return $this->cellValueFormatter->extractAndFormatNodeValue($node);
216
    }
217
218
    /**
219
     * Return the current element, from the buffer.
220
     * @link http://php.net/manual/en/iterator.current.php
221
     *
222
     * @return array|null
223
     */
224 81
    public function current()
225
    {
226 81
        return $this->rowDataBuffer;
227
    }
228
229
    /**
230
     * Return the key of the current element
231
     * @link http://php.net/manual/en/iterator.key.php
232
     *
233
     * @return int
234
     */
235 78
    public function key()
236
    {
237 78
        return $this->numReadRows;
238
    }
239
240
241
    /**
242
     * Cleans up what was created to iterate over the object.
243
     *
244
     * @return void
245
     */
246 87
    public function end()
247
    {
248 87
        $this->xmlReader->close();
249 87
    }
250
}
251