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

RowIterator::getCellValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
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\ReaderOptions;
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_CELL_INDEX = 'r';
31
32
    /** @var string Path of the XLSX file being read */
33
    protected $filePath;
34
35
    /** @var string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml */
36
    protected $sheetDataXMLFilePath;
37
38
    /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */
39
    protected $xmlReader;
40
41
    /** @var Helper\CellValueFormatter Helper to format cell values */
42
    protected $cellValueFormatter;
43
44
    /** @var Helper\StyleHelper $styleHelper Helper to work with styles */
45
    protected $styleHelper;
46
47
    /** @var int Key for iterator */
48
    protected $rowIndex = 0;
49
50
    /** @var array Buffer used to store the row data, while checking if there are more rows to read */
51
    protected $rowDataBuffer = [];
52
53
    /** @var bool Indicates whether all rows have been read */
54
    protected $hasReachedEndOfFile = false;
55
56
    /** @var int The number of columns the sheet has (0 meaning undefined) */
57
    protected $numColumns = 0;
58
59
    /** @var \Box\Spout\Reader\ReaderOptions */
60
    protected $readerOptions;
61
62
    /**
63
     * @param string $filePath Path of the XLSX file being read
64
     * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
65
     * @param Helper\SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
66
     * @param \Box\Spout\Reader\ReaderOptions $readerOptions
67
     */
68 90
    public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, ReaderOptions $readerOptions)
69
    {
70 90
        $this->filePath = $filePath;
71 90
        $this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
72
73 90
        $this->xmlReader = new XMLReader();
74
75 90
        $this->styleHelper = new StyleHelper($filePath);
76 90
        $this->readerOptions = $readerOptions;
77 90
        $this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper, $readerOptions->shouldFormatDates());
78 90
    }
79
80
    /**
81
     * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
82
     * @return string Path of the XML file containing the sheet data,
83
     *                without the leading slash.
84
     */
85 90
    protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath)
86
    {
87 90
        return ltrim($sheetDataXMLFilePath, '/');
88
    }
89
90
    /**
91
     * Rewind the Iterator to the first element.
92
     * Initializes the XMLReader object that reads the associated sheet data.
93
     * The XMLReader is configured to be safe from billion laughs attack.
94
     * @link http://php.net/manual/en/iterator.rewind.php
95
     *
96
     * @return void
97
     * @throws \Box\Spout\Common\Exception\IOException If the sheet data XML cannot be read
98
     */
99 87
    public function rewind()
100
    {
101 87
        $this->xmlReader->close();
102
103 87
        $sheetDataFilePath = 'zip://' . $this->filePath . '#' . $this->sheetDataXMLFilePath;
104 87
        if ($this->xmlReader->open($sheetDataFilePath) === false) {
105 3
            throw new IOException("Could not open \"{$this->sheetDataXMLFilePath}\".");
106
        }
107
108 84
        $this->numReadRows = 0;
0 ignored issues
show
Bug introduced by
The property numReadRows does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
109 84
        $this->rowDataBuffer = [];
110 84
        $this->hasReachedEndOfFile = false;
111 84
        $this->numColumns = 0;
112
113 84
        $this->next();
114 84
    }
115
116
    /**
117
     * Checks if current position is valid
118
     * @link http://php.net/manual/en/iterator.valid.php
119
     *
120
     * @return boolean
121
     */
122 84
    public function valid()
123
    {
124 84
        return (!$this->hasReachedEndOfFile);
125
    }
126
127
    /**
128
     * Move forward to next element. Empty rows will be skipped.
129
     * @link http://php.net/manual/en/iterator.next.php
130
     *
131
     * @return void
132
     * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
133
     * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
134
     */
135 84
    public function next()
136
    {
137 84
        $rowData = [];
138
139 84
        if (count($this->rowDataBuffer) > 1) {
140 6
            array_shift($this->rowDataBuffer);
141 6
            $this->rowIndex++;
142
143 6
            return;
144
        } else {
145 84
            $this->rowDataBuffer = [];
146
        }
147
148
        try {
149 84
            while ($this->xmlReader->read()) {
150 84
                if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_DIMENSION)) {
151
                    // Read dimensions of the sheet
152 42
                    $dimensionRef = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet)
153 42
                    if (preg_match('/[A-Z\d]+:([A-Z\d]+)/', $dimensionRef, $matches)) {
154 36
                        $lastCellIndex = $matches[1];
155 36
                        $this->numColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
156 36
                    }
157
158 84
                } elseif ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_ROW)) {
159
                    // Start of the row description
160 81
                    $prevRowIndex = $this->rowIndex;
161 81
                    $newRowIndex = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX);
162
163
                    // Read spans info if present
164 81
                    $numberOfColumnsForRow = $this->numColumns;
165 81
                    $spans = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance
166 81
                    if ($spans) {
167 30
                        list(, $numberOfColumnsForRow) = explode(':', $spans);
168 30
                        $numberOfColumnsForRow = intval($numberOfColumnsForRow);
169 30
                    }
170 81
                    $rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
171
172 81
                    if ($this->readerOptions->shouldPreserveEmptyRows()) {
173 6
                        for ($i = $prevRowIndex + 1; $i < $newRowIndex; ++$i) {
174 6
                            $this->rowDataBuffer[] = $rowData; // fake empty rows
175 6
                        }
176 6
                    }
177
178 81
                    $this->rowIndex = $newRowIndex - count($this->rowDataBuffer);
0 ignored issues
show
Documentation Bug introduced by
It seems like $newRowIndex - count($this->rowDataBuffer) can also be of type double. However, the property $rowIndex is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
179
180 84
                } elseif ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL)) {
181
                    // Start of a cell description
182 81
                    $currentCellIndex = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX);
183 81
                    $currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex);
184
185 81
                    $node = $this->xmlReader->expand();
186 81
                    $rowData[$currentColumnIndex] = $this->getCellValue($node);
187
188 84
                } elseif ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) {
189
                    // End of the row description
190
                    // If needed, we fill the empty cells
191 81
                    $rowData = ($this->numColumns !== 0) ? $rowData : CellHelper::fillMissingArrayIndexes($rowData);
192
193 81
                    break;
194
195 84
                } elseif ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_WORKSHEET)) {
196
                    // The closing "</worksheet>" marks the end of the file
197 81
                    $this->hasReachedEndOfFile = true;
198 81
                    break;
199
                }
200 84
            }
201
202 84
        } catch (XMLProcessingException $exception) {
203
            throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]");
204
        }
205
206 84
        $this->rowDataBuffer[] = $rowData;
207 84
    }
208
209
    /**
210
     * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
211
     *
212
     * @param \DOMNode $node
213
     * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error)
214
     */
215 81
    protected function getCellValue($node)
216
    {
217 81
        return $this->cellValueFormatter->extractAndFormatNodeValue($node);
218
    }
219
220
    /**
221
     * Return the current element, from the buffer.
222
     * @link http://php.net/manual/en/iterator.current.php
223
     *
224
     * @return array|null
225
     */
226 81
    public function current()
227
    {
228 81
        return isset($this->rowDataBuffer[0]) ? $this->rowDataBuffer[0] : null;
229
    }
230
231
    /**
232
     * Return the key of the current element
233
     * @link http://php.net/manual/en/iterator.key.php
234
     *
235
     * @return int
236
     */
237 9
    public function key()
238
    {
239 9
        return $this->rowIndex;
240
    }
241
242
243
    /**
244
     * Cleans up what was created to iterate over the object.
245
     *
246
     * @return void
247
     */
248 87
    public function end()
249
    {
250 87
        $this->xmlReader->close();
251 87
    }
252
}
253