Completed
Pull Request — master (#773)
by
unknown
12:09
created

WorksheetManager::getCellFormulaXMLFragment()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2.1481

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 2
cts 3
cp 0.6667
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2.1481
1
<?php
2
3
namespace Box\Spout\Writer\XLSX\Manager;
4
5
use Box\Spout\Common\Entity\Cell;
6
use Box\Spout\Common\Entity\Row;
7
use Box\Spout\Common\Entity\Style\Style;
8
use Box\Spout\Common\Exception\InvalidArgumentException;
9
use Box\Spout\Common\Exception\IOException;
10
use Box\Spout\Common\Helper\Escaper\XLSX as XLSXEscaper;
11
use Box\Spout\Common\Helper\StringHelper;
12
use Box\Spout\Common\Manager\OptionsManagerInterface;
13
use Box\Spout\Writer\Common\Creator\InternalEntityFactory;
14
use Box\Spout\Writer\Common\Entity\Options;
15
use Box\Spout\Writer\Common\Entity\Worksheet;
16
use Box\Spout\Writer\Common\Helper\CellHelper;
17
use Box\Spout\Writer\Common\Manager\RowManager;
18
use Box\Spout\Writer\Common\Manager\Style\StyleMerger;
19
use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface;
20
use Box\Spout\Writer\XLSX\Manager\Style\StyleManager;
21
22
/**
23
 * Class WorksheetManager
24
 * XLSX worksheet manager, providing the interfaces to work with XLSX worksheets.
25
 */
26
class WorksheetManager implements WorksheetManagerInterface
27
{
28
    /**
29
     * Maximum number of characters a cell can contain
30
     * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-16c69c74-3d6a-4aaf-ba35-e6eb276e8eaa [Excel 2007]
31
     * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3 [Excel 2010]
32
     * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-ca36e2dc-1f09-4620-b726-67c00b05040f [Excel 2013/2016]
33
     */
34
    const MAX_CHARACTERS_PER_CELL = 32767;
35
36
    const SHEET_XML_FILE_HEADER = <<<'EOD'
37
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
38
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
39
EOD;
40
41
    /** @var bool Whether inline or shared strings should be used */
42
    protected $shouldUseInlineStrings;
43
44
    /** @var RowManager Manages rows */
45
    private $rowManager;
46
47
    /** @var StyleManager Manages styles */
48
    private $styleManager;
49
50
    /** @var StyleMerger Helper to merge styles together */
51
    private $styleMerger;
52
53
    /** @var SharedStringsManager Helper to write shared strings */
54
    private $sharedStringsManager;
55
56
    /** @var XLSXEscaper Strings escaper */
57
    private $stringsEscaper;
58
59
    /** @var StringHelper String helper */
60
    private $stringHelper;
61
62
    /** @var InternalEntityFactory Factory to create entities */
63
    private $entityFactory;
64
65
    /**
66
     * WorksheetManager constructor.
67
     *
68
     * @param OptionsManagerInterface $optionsManager
69
     * @param RowManager $rowManager
70
     * @param StyleManager $styleManager
71
     * @param StyleMerger $styleMerger
72
     * @param SharedStringsManager $sharedStringsManager
73
     * @param XLSXEscaper $stringsEscaper
74
     * @param StringHelper $stringHelper
75
     * @param InternalEntityFactory $entityFactory
76
     */
77 42
    public function __construct(
78
        OptionsManagerInterface $optionsManager,
79
        RowManager $rowManager,
80
        StyleManager $styleManager,
81
        StyleMerger $styleMerger,
82
        SharedStringsManager $sharedStringsManager,
83
        XLSXEscaper $stringsEscaper,
84
        StringHelper $stringHelper,
85
        InternalEntityFactory $entityFactory
86
    ) {
87 42
        $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS);
88 42
        $this->rowManager = $rowManager;
89 42
        $this->styleManager = $styleManager;
90 42
        $this->styleMerger = $styleMerger;
91 42
        $this->sharedStringsManager = $sharedStringsManager;
92 42
        $this->stringsEscaper = $stringsEscaper;
93 42
        $this->stringHelper = $stringHelper;
94 42
        $this->entityFactory = $entityFactory;
95 42
    }
96
97
    /**
98
     * @return SharedStringsManager
99
     */
100 38
    public function getSharedStringsManager()
101
    {
102 38
        return $this->sharedStringsManager;
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108 42
    public function startSheet(Worksheet $worksheet)
109
    {
110 42
        $sheetFilePointer = \fopen($worksheet->getFilePath(), 'w');
111 42
        $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
112
113 42
        $worksheet->setFilePointer($sheetFilePointer);
114
115 42
        \fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER);
116 42
        \fwrite($sheetFilePointer, '<sheetData>');
117 42
    }
118
119
    /**
120
     * Checks if the sheet has been sucessfully created. Throws an exception if not.
121
     *
122
     * @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file
123
     * @throws IOException If the sheet data file cannot be opened for writing
124
     * @return void
125
     */
126 42
    private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
127
    {
128 42
        if (!$sheetFilePointer) {
129
            throw new IOException('Unable to open sheet for writing.');
130
        }
131 42
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 35
    public function addRow(Worksheet $worksheet, Row $row)
137
    {
138 35
        if (!$this->rowManager->isEmpty($row)) {
139 35
            $this->addNonEmptyRow($worksheet, $row);
140
        }
141
142 34
        $worksheet->setLastWrittenRowIndex($worksheet->getLastWrittenRowIndex() + 1);
143 34
    }
144
145
    /**
146
     * Adds non empty row to the worksheet.
147
     *
148
     * @param Worksheet $worksheet The worksheet to add the row to
149
     * @param Row $row The row to be written
150
     * @throws IOException If the data cannot be written
151
     * @throws InvalidArgumentException If a cell value's type is not supported
152
     * @return void
153
     */
154 35
    private function addNonEmptyRow(Worksheet $worksheet, Row $row)
155
    {
156 35
        $rowStyle = $row->getStyle();
157 35
        $rowIndexOneBased = $worksheet->getLastWrittenRowIndex() + 1;
158 35
        $numCells = $row->getNumCells();
159
160 35
        $rowXML = '<row r="' . $rowIndexOneBased . '" spans="1:' . $numCells . '">';
161
162 35
        foreach ($row->getCells() as $columnIndexZeroBased => $cell) {
163 35
            $rowXML .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $rowIndexOneBased, $columnIndexZeroBased);
164
        }
165
166 34
        $rowXML .= '</row>';
167
168 34
        $wasWriteSuccessful = \fwrite($worksheet->getFilePointer(), $rowXML);
169 34
        if ($wasWriteSuccessful === false) {
170
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
171
        }
172 34
    }
173
174
    /**
175
     * Applies styles to the given style, merging the cell's style with its row's style
176
     * Then builds and returns xml for the cell.
177
     *
178
     * @param Cell  $cell
179
     * @param Style $rowStyle
180
     * @param int   $rowIndexOneBased
181
     * @param int   $columnIndexZeroBased
182
     *
183
     * @throws InvalidArgumentException If the given value cannot be processed
184
     * @return string
185
     */
186 35
    private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $rowIndexOneBased, $columnIndexZeroBased)
187
    {
188
        // Apply row and extra styles
189 35
        $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
190 35
        $cell->setStyle($mergedCellAndRowStyle);
191 35
        $newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
192
193 35
        $registeredStyle = $this->styleManager->registerStyle($newCellStyle);
194
195 35
        return $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $registeredStyle->getId());
196
    }
197
198
    /**
199
     * Builds and returns xml for a single cell.
200
     *
201
     * @param int  $rowIndexOneBased
202
     * @param int  $columnIndexZeroBased
203
     * @param Cell $cell
204
     * @param int  $styleId
205
     *
206
     * @throws InvalidArgumentException If the given value cannot be processed
207
     * @return string
208
     */
209 35
    private function getCellXML($rowIndexOneBased, $columnIndexZeroBased, Cell $cell, $styleId)
210
    {
211 35
        $columnLetters = CellHelper::getColumnLettersFromColumnIndex($columnIndexZeroBased);
212 35
        $cellXML = '<c r="' . $columnLetters . $rowIndexOneBased . '"';
213 35
        $cellXML .= ' s="' . $styleId . '"';
214
        $cellFormulaXMLFragment = $this->getCellFormulaXMLFragment($cell);
215 35
216 32
        if ($cell->isString()) {
217 6
            $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell);
218 1
        } elseif ($cell->isBoolean()) {
219 6
            $cellXML .= ' t="b">' . $cellFormulaXMLFragment . '<v>' . (int) ($cell->getValue()) . '</v></c>';
220 2
        } elseif ($cell->isNumeric()) {
221 5
            $cellXML .= '>' . $cellFormulaXMLFragment . '<v>' . $cell->getValue() . '</v></c>';
222
        } elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) {
223 1
            // only writes the error value if it's a string
224 4
            $cellXML .= ' t="e"><v>' . $cell->getValueEvenIfError() . '</v></c>';
225 2
        } elseif ($cell->isEmpty()) {
226 1
            if ($cellFormulaXMLFragment) {
227
                $cellXML .= '>' . $cellFormulaXMLFragment . '<v></v></c>';
228
            } else if ($this->styleManager->shouldApplyStyleOnEmptyCell($styleId)) {
229
                $cellXML .= '/>';
230 2
            } else {
231
                // don't write empty cells that do no need styling
232
                // NOTE: not appending to $cellXML is the right behavior!!
233 2
                $cellXML = '';
234
            }
235
        } else {
236 34
            throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . \gettype($cell->getValue()));
237
        }
238
239
        return $cellXML;
240
    }
241
242
    /**
243
     * Returns the XML fragment for a cell formula
244
     *
245
     * @param string $cellValue The cell value
0 ignored issues
show
Bug introduced by
There is no parameter named $cellValue. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
246 32
     * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell
247
     * @return string The XML fragment representing the cell
248 32
     */
249
    private function getCellFormulaXMLFragment($cell)
250
    {
251
        $cellFormula = $cell->getFormula();
252 32
        return $cellFormula ? '<f>' . $cellFormula . '</f>' : '';
253 29
    }
254
255 3
    /**
256 3
     * Returns the XML fragment for a cell containing a non empty string
257
     *
258
     * @param Cell $cell cell
259 32
     * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell
260
     * @return string The XML fragment representing the cell
261
     */
262
    private function getCellXMLFragmentForNonEmptyString($cell)
263
    {
264
        $cellValue = $cell->getValue();
265 38
        $cellFormulaXMLFragment = $this->getCellFormulaXMLFragment($cell);
266
267 38
        if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) {
268
            throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)');
269 38
        }
270
271
        if ($cellFormulaXMLFragment) {
272
            $cellXMLFragment = ' t="str">' . $cellFormulaXMLFragment . '<v>' . $cellValue . '</v></c>';
273 38
        } else if ($this->shouldUseInlineStrings) {
274 38
            $cellXMLFragment = ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
275 38
        } else {
276 38
            $sharedStringId = $this->sharedStringsManager->writeString($cellValue);
277
            $cellXMLFragment = ' t="s"><v>' . $sharedStringId . '</v></c>';
278
        }
279
280
        return $cellXMLFragment;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286
    public function close(Worksheet $worksheet)
287
    {
288
        $worksheetFilePointer = $worksheet->getFilePointer();
289
290
        if (!\is_resource($worksheetFilePointer)) {
291
            return;
292
        }
293
294
        \fwrite($worksheetFilePointer, '</sheetData>');
295
        \fwrite($worksheetFilePointer, '</worksheet>');
296
        \fclose($worksheetFilePointer);
297
    }
298
}
299