Completed
Pull Request — develop_3.0 (#427)
by Adrien
04:16 queued 01:53
created

WorksheetManager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 11
nc 1
nop 5
crap 1
1
<?php
2
3
namespace Box\Spout\Writer\XLSX\Manager;
4
5
use Box\Spout\Common\Exception\InvalidArgumentException;
6
use Box\Spout\Common\Exception\IOException;
7
use Box\Spout\Common\Helper\StringHelper;
8
use Box\Spout\Writer\Common\Cell;
9
use Box\Spout\Writer\Common\Helper\CellHelper;
10
use Box\Spout\Writer\Common\Manager\OptionsManagerInterface;
11
use Box\Spout\Writer\Common\Options;
12
use Box\Spout\Writer\Entity\Worksheet;
13
use Box\Spout\Writer\Manager\WorksheetManagerInterface;
14
use Box\Spout\Writer\Style\Style;
15
use Box\Spout\Writer\XLSX\Helper\SharedStringsHelper;
16
use Box\Spout\Writer\XLSX\Helper\StyleHelper;
17
18
/**
19
 * Class WorksheetManager
20
 * XLSX worksheet manager, providing the interfaces to work with XLSX worksheets.
21
 *
22
 * @package Box\Spout\Writer\XLSX\Manager
23
 */
24
class WorksheetManager implements WorksheetManagerInterface
25
{
26
    /**
27
     * Maximum number of characters a cell can contain
28
     * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-16c69c74-3d6a-4aaf-ba35-e6eb276e8eaa [Excel 2007]
29
     * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3 [Excel 2010]
30
     * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-ca36e2dc-1f09-4620-b726-67c00b05040f [Excel 2013/2016]
31
     */
32
    const MAX_CHARACTERS_PER_CELL = 32767;
33
34
    const SHEET_XML_FILE_HEADER = <<<EOD
35
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
36
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
37
EOD;
38
39
    /** @var bool Whether inline or shared strings should be used */
40
    protected $shouldUseInlineStrings;
41
42
    /** @var SharedStringsHelper Helper to write shared strings */
43
    private $sharedStringsHelper;
44
45
    /** @var StyleHelper Helper to work with styles */
46
    private $styleHelper;
47
48
    /** @var \Box\Spout\Common\Escaper\XLSX Strings escaper */
49
    private $stringsEscaper;
50
51
    /** @var StringHelper String helper */
52
    private $stringHelper;
53
54
    /**
55
     * WorksheetManager constructor.
56
     *
57
     * @param OptionsManagerInterface $optionsManager
58
     * @param SharedStringsHelper $sharedStringsHelper
59
     * @param StyleHelper $styleHelper
60
     * @param \Box\Spout\Common\Escaper\XLSX $stringsEscaper
61
     * @param StringHelper $stringHelper
62
     */
63 46
    public function __construct(
64
        OptionsManagerInterface $optionsManager,
65
        SharedStringsHelper $sharedStringsHelper,
66
        StyleHelper $styleHelper,
67
        \Box\Spout\Common\Escaper\XLSX $stringsEscaper,
68
        StringHelper $stringHelper)
69
    {
70 46
        $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS);
71 46
        $this->sharedStringsHelper = $sharedStringsHelper;
72 46
        $this->styleHelper = $styleHelper;
73 46
        $this->stringsEscaper = $stringsEscaper;
74 46
        $this->stringHelper = $stringHelper;
75 46
    }
76
77
    /**
78
     * @return SharedStringsHelper
79
     */
80 36
    public function getSharedStringsHelper()
81
    {
82 36
        return $this->sharedStringsHelper;
83
    }
84
85
86
    /**
87
     * Prepares the worksheet to accept data
88
     *
89
     * @param Worksheet $worksheet The worksheet to start
90
     * @return void
91
     * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
92
     */
93 46
    public function startSheet(Worksheet $worksheet)
94
    {
95 46
        $sheetFilePointer = fopen($worksheet->getFilePath(), 'w');
96 46
        $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
97
98 46
        $worksheet->setFilePointer($sheetFilePointer);
99
100 46
        fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER);
101 46
        fwrite($sheetFilePointer, '<sheetData>');
102 46
    }
103
104
    /**
105
     * Checks if the sheet has been sucessfully created. Throws an exception if not.
106
     *
107
     * @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file
108
     * @return void
109
     * @throws IOException If the sheet data file cannot be opened for writing
110
     */
111 46
    private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
112
    {
113 46
        if (!$sheetFilePointer) {
114
            throw new IOException('Unable to open sheet for writing.');
115
        }
116 46
    }
117
118
    /**
119
     * Adds data to the given worksheet.
120
     *
121
     * @param Worksheet $worksheet The worksheet to add the row to
122
     * @param array $dataRow Array containing data to be written. Cannot be empty.
123
     *          Example $dataRow = ['data1', 1234, null, '', 'data5'];
124
     * @param Style $rowStyle Style to be applied to the row. NULL means use default style.
125
     * @return void
126
     * @throws IOException If the data cannot be written
127
     * @throws InvalidArgumentException If a cell value's type is not supported
128
     */
129 33
    public function addRow(Worksheet $worksheet, $dataRow, $rowStyle)
130
    {
131 33
        if (!$this->isEmptyRow($dataRow)) {
132 33
            $this->addNonEmptyRow($worksheet, $dataRow, $rowStyle);
133
        }
134
135 31
        $worksheet->setLastWrittenRowIndex($worksheet->getLastWrittenRowIndex() + 1);
136 31
    }
137
138
    /**
139
     * Returns whether the given row is empty
140
     *
141
     * @param array $dataRow Array containing data to be written. Cannot be empty.
142
     *          Example $dataRow = ['data1', 1234, null, '', 'data5'];
143
     * @return bool Whether the given row is empty
144
     */
145 33
    private function isEmptyRow($dataRow)
146
    {
147 33
        $numCells = count($dataRow);
148
        // using "reset()" instead of "$dataRow[0]" because $dataRow can be an associative array
149 33
        return ($numCells === 1 && CellHelper::isEmpty(reset($dataRow)));
150
    }
151
152
    /**
153
     * Adds non empty row to the worksheet.
154
     *
155
     * @param Worksheet $worksheet The worksheet to add the row to
156
     * @param array $dataRow Array containing data to be written. Cannot be empty.
157
     *          Example $dataRow = ['data1', 1234, null, '', 'data5'];
158
     * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style.
159
     * @return void
160
     * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
161
     * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
162
     */
163 33
    private function addNonEmptyRow(Worksheet $worksheet, $dataRow, $style)
164
    {
165 33
        $cellNumber = 0;
166 33
        $rowIndex = $worksheet->getLastWrittenRowIndex() + 1;
167 33
        $numCells = count($dataRow);
168
169 33
        $rowXML = '<row r="' . $rowIndex . '" spans="1:' . $numCells . '">';
170
171 33
        foreach($dataRow as $cellValue) {
172 33
            $rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cellValue, $style->getId());
173 31
            $cellNumber++;
174
        }
175
176 31
        $rowXML .= '</row>';
177
178 31
        $wasWriteSuccessful = fwrite($worksheet->getFilePointer(), $rowXML);
179 31
        if ($wasWriteSuccessful === false) {
180
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
181
        }
182 31
    }
183
184
    /**
185
     * Build and return xml for a single cell.
186
     *
187
     * @param int $rowIndex
188
     * @param int $cellNumber
189
     * @param mixed $cellValue
190
     * @param int $styleId
191
     * @return string
192
     * @throws InvalidArgumentException If the given value cannot be processed
193
     */
194 33
    private function getCellXML($rowIndex, $cellNumber, $cellValue, $styleId)
195
    {
196 33
        $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
197 33
        $cellXML = '<c r="' . $columnIndex . $rowIndex . '"';
198 33
        $cellXML .= ' s="' . $styleId . '"';
199
200
        /** @TODO Remove code duplication with ODS writer: https://github.com/box/spout/pull/383#discussion_r113292746 */
201 33
        if ($cellValue instanceof Cell) {
202 2
            $cell = $cellValue;
203
        } else {
204 31
            $cell = new Cell($cellValue);
205
        }
206
207 33
        if ($cell->isString()) {
208 32
            $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell->getValue());
209 5
        } else if ($cell->isBoolean()) {
210 2
            $cellXML .= ' t="b"><v>' . intval($cell->getValue()) . '</v></c>';
211 5
        } else if ($cell->isNumeric()) {
212 2
            $cellXML .= '><v>' . $cell->getValue() . '</v></c>';
213 4
        } else if ($cell->isEmpty()) {
214 2
            if ($this->styleHelper->shouldApplyStyleOnEmptyCell($styleId)) {
215 1
                $cellXML .= '/>';
216
            } else {
217
                // don't write empty cells that do no need styling
218
                // NOTE: not appending to $cellXML is the right behavior!!
219 2
                $cellXML = '';
220
            }
221
        } else {
222 2
            throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cell->getValue()));
223
        }
224
225 31
        return $cellXML;
226
    }
227
228
    /**
229
     * Returns the XML fragment for a cell containing a non empty string
230
     *
231
     * @param string $cellValue The cell value
232
     * @return string The XML fragment representing the cell
233
     * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell
234
     */
235 32
    private function getCellXMLFragmentForNonEmptyString($cellValue)
236
    {
237 32
        if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) {
238 1
            throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)');
239
        }
240
241 31
        if ($this->shouldUseInlineStrings) {
242 26
            $cellXMLFragment = ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
243
        } else {
244 5
            $sharedStringId = $this->sharedStringsHelper->writeString($cellValue);
245 5
            $cellXMLFragment = ' t="s"><v>' . $sharedStringId . '</v></c>';
246
        }
247
248 31
        return $cellXMLFragment;
249
    }
250
251
    /**
252
     * Closes the worksheet
253
     *
254
     * @param Worksheet $worksheet
255
     * @return void
256
     */
257 36
    public function close(Worksheet $worksheet)
258
    {
259 36
        $worksheetFilePointer = $worksheet->getFilePointer();
260
261 36
        if (!is_resource($worksheetFilePointer)) {
262
            return;
263
        }
264
265 36
        fwrite($worksheetFilePointer, '</sheetData>');
266 36
        fwrite($worksheetFilePointer, '</worksheet>');
267 36
        fclose($worksheetFilePointer);
268
    }
269
}