Completed
Pull Request — master (#235)
by
unknown
03:20
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 87
    public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, ReaderOptions $readerOptions)
69
    {
70 87
        $this->filePath = $filePath;
71 87
        $this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
72
73 87
        $this->xmlReader = new XMLReader();
74
75 87
        $this->styleHelper = new StyleHelper($filePath);
76 87
        $this->readerOptions = $readerOptions;
77 87
        $this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper, $readerOptions->shouldFormatDates());
78 87
    }
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 87
    protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath)
86
    {
87 87
        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 84
    public function rewind()
100
    {
101 84
        $this->xmlReader->close();
102
103 84
        $sheetDataFilePath = 'zip://' . $this->filePath . '#' . $this->sheetDataXMLFilePath;
104 84
        if ($this->xmlReader->open($sheetDataFilePath) === false) {
105 3
            throw new IOException("Could not open \"{$this->sheetDataXMLFilePath}\".");
106
        }
107
108 81
        $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 81
        $this->rowDataBuffer = [];
110 81
        $this->hasReachedEndOfFile = false;
111 81
        $this->numColumns = 0;
112
113 81
        $this->next();
114 81
    }
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 81
    public function valid()
123
    {
124 81
        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 81
    public function next()
136
    {
137 81
        $rowData = [];
138
139 81
        if (count($this->rowDataBuffer) > 1) {
140 3
            array_shift($this->rowDataBuffer);
141 3
            $this->rowIndex++;
142
143 3
            return;
144
        } else {
145 81
            $this->rowDataBuffer = [];
146
        }
147
148
        try {
149 81
            while ($this->xmlReader->read()) {
150 81
                if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_DIMENSION)) {
151
                    // Read dimensions of the sheet
152 39
                    $dimensionRef = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet)
153 39
                    if (preg_match('/[A-Z\d]+:([A-Z\d]+)/', $dimensionRef, $matches)) {
154 33
                        $lastCellIndex = $matches[1];
155 33
                        $this->numColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
156 33
                    }
157
158 81
                } else if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_ROW)) {
159
                    // Start of the row description
160 78
                    $prevRowIndex = $this->rowIndex;
161 78
                    $newRowIndex = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX);
162
163
                    // Read spans info if present
164 78
                    $numberOfColumnsForRow = $this->numColumns;
165 78
                    $spans = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance
166 78
                    if ($spans) {
167 27
                        list(, $numberOfColumnsForRow) = explode(':', $spans);
168 27
                        $numberOfColumnsForRow = intval($numberOfColumnsForRow);
169 27
                    }
170 78
                    $rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
171
172 78
                    if ($this->readerOptions->shouldPreserveEmptyRows()) {
173 3
                        for ($i = $prevRowIndex + 1; $i < $newRowIndex; ++$i) {
174 3
                            $this->rowDataBuffer[] = $rowData; // fake empty rows
175 3
                        }
176 3
                    }
177
178 78
                    $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 81
                } else if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL)) {
181
                    // Start of a cell description
182 78
                    $currentCellIndex = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX);
183 78
                    $currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex);
184
185 78
                    $node = $this->xmlReader->expand();
186 78
                    $rowData[$currentColumnIndex] = $this->getCellValue($node);
187
188 81
                } else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) {
189
                    // End of the row description
190
                    // If needed, we fill the empty cells
191 78
                    $rowData = ($this->numColumns !== 0) ? $rowData : CellHelper::fillMissingArrayIndexes($rowData);
192
193 78
                    break;
194
195 81
                } else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_WORKSHEET)) {
196
                    // The closing "</worksheet>" marks the end of the file
197 78
                    $this->hasReachedEndOfFile = true;
198 78
                    break;
199
                }
200 81
            }
201
202 81
        } catch (XMLProcessingException $exception) {
203
            throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]");
204
        }
205
206 81
        $this->rowDataBuffer[] = $rowData;
207 81
    }
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 78
    protected function getCellValue($node)
216
    {
217 78
        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 78
    public function current()
227
    {
228 78
        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 6
    public function key()
238
    {
239 6
        return $this->rowIndex;
240
    }
241
242
243
    /**
244
     * Cleans up what was created to iterate over the object.
245
     *
246
     * @return void
247
     */
248 84
    public function end()
249
    {
250 84
        $this->xmlReader->close();
251 84
    }
252
}
253