Failed Conditions
Pull Request — develop_3.0 (#434)
by Hura
03:16
created

WorksheetManager::getCellXML()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 26
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 0
cts 16
cp 0
rs 8.439
c 0
b 0
f 0
cc 6
eloc 18
nc 6
nop 4
crap 42
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\Helper\CellHelper;
9
use Box\Spout\Writer\Common\Manager\OptionsManagerInterface;
10
use Box\Spout\Writer\Common\Entity\Options;
11
use Box\Spout\Writer\Common\Entity\Cell;
12
use Box\Spout\Writer\Common\Entity\Row;
13
use Box\Spout\Writer\Common\Entity\Worksheet;
14
use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface;
15
use Box\Spout\Writer\XLSX\Manager\Style\StyleManager;
16
17
/**
18
 * Class WorksheetManager
19
 * XLSX worksheet manager, providing the interfaces to work with XLSX worksheets.
20
 *
21
 * @package Box\Spout\Writer\XLSX\Manager
22
 */
23
class WorksheetManager implements WorksheetManagerInterface
24
{
25
    /**
26
     * Maximum number of characters a cell can contain
27
     * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-16c69c74-3d6a-4aaf-ba35-e6eb276e8eaa [Excel 2007]
28
     * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3 [Excel 2010]
29
     * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-ca36e2dc-1f09-4620-b726-67c00b05040f [Excel 2013/2016]
30
     */
31
    const MAX_CHARACTERS_PER_CELL = 32767;
32
33
    const SHEET_XML_FILE_HEADER = <<<EOD
34
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
35
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
36
EOD;
37
38
    /** @var bool Whether inline or shared strings should be used */
39
    protected $shouldUseInlineStrings;
40
41
    /** @var StyleManager Manages styles */
42
    private $styleManager;
43
44
    /** @var SharedStringsManager Helper to write shared strings */
45
    private $sharedStringsManager;
46
47
    /** @var \Box\Spout\Common\Escaper\XLSX Strings escaper */
48
    private $stringsEscaper;
49
50
    /** @var StringHelper String helper */
51
    private $stringHelper;
52
53
    /**
54
     * WorksheetManager constructor.
55
     *
56
     * @param OptionsManagerInterface $optionsManager
57
     * @param StyleManager $styleManager
58
     * @param SharedStringsManager $sharedStringsManager
59
     * @param \Box\Spout\Common\Escaper\XLSX $stringsEscaper
60
     * @param StringHelper $stringHelper
61
     */
62 40
    public function __construct(
63
        OptionsManagerInterface $optionsManager,
64
        StyleManager $styleManager,
65
        SharedStringsManager $sharedStringsManager,
66
        \Box\Spout\Common\Escaper\XLSX $stringsEscaper,
67
        StringHelper $stringHelper)
68
    {
69 40
        $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS);
70 40
        $this->styleManager = $styleManager;
71 40
        $this->sharedStringsManager = $sharedStringsManager;
72 40
        $this->stringsEscaper = $stringsEscaper;
73 40
        $this->stringHelper = $stringHelper;
74 40
    }
75
76
    /**
77
     * @return SharedStringsManager
78
     */
79 4
    public function getSharedStringsManager()
80
    {
81 4
        return $this->sharedStringsManager;
82
    }
83
84
85
    /**
86
     * Prepares the worksheet to accept data
87
     *
88
     * @param Worksheet $worksheet The worksheet to start
89
     * @return void
90
     * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
91
     */
92 40
    public function startSheet(Worksheet $worksheet)
93
    {
94 40
        $sheetFilePointer = fopen($worksheet->getFilePath(), 'w');
95 40
        $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
96
97 40
        $worksheet->setFilePointer($sheetFilePointer);
98
99 40
        fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER);
100 40
        fwrite($sheetFilePointer, '<sheetData>');
101 40
    }
102
103
    /**
104
     * Checks if the sheet has been sucessfully created. Throws an exception if not.
105
     *
106
     * @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file
107
     * @return void
108
     * @throws IOException If the sheet data file cannot be opened for writing
109
     */
110 40
    private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
111
    {
112 40
        if (!$sheetFilePointer) {
113
            throw new IOException('Unable to open sheet for writing.');
114
        }
115 40
    }
116
117
    /**
118
     * Adds a row to the worksheet.
119
     *
120
     * @param Worksheet $worksheet The worksheet to add the row to
121
     * @param Row $row The row to be added
122
     * @return void
123
     * @throws IOException If the data cannot be written
124
     * @throws InvalidArgumentException If a cell value's type is not supported
125
     */
126
    public function addRow(Worksheet $worksheet, Row $row)
127
    {
128
        if (!$row->isEmpty()) {
129
            $this->addNonEmptyRow($worksheet, $row);
130
        }
131
132
        $worksheet->setLastWrittenRowIndex($worksheet->getLastWrittenRowIndex() + 1);
133
    }
134
135
    /**
136
     * Adds non empty row to the worksheet.
137
     *
138
     * @param Row $row The row to be written
139
     * @return void
140
     *
141
     * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
142
     * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
143
     */
144
    private function addNonEmptyRow(Worksheet $worksheet, Row $row)
145
    {
146
        $cellNumber = 0;
147
        $rowIndex = $worksheet->getLastWrittenRowIndex() + 1;
148
        $numCells = count($row->getCells());
149
150
        $rowXML = '<row r="' . $rowIndex . '" spans="1:' . $numCells . '">';
151
152
        // @TODO refactoring: move this to its own method
153
        /** @var Cell $cell */
154
        foreach($row->getCells() as $cell) {
155
            // Apply styles - the row style is merged at this point
156
            $cell->applyStyle($row->getStyle());
157
            $this->styleManager->applyExtraStylesIfNeeded($cell);
158
            $registeredStyle = $this->styleManager->registerStyle($cell->getStyle());
0 ignored issues
show
Bug introduced by
It seems like $cell->getStyle() can be null; however, registerStyle() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
159
            $rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cell, $registeredStyle->getId());
160
            $cellNumber++;
161
        }
162
163
        $rowXML .= '</row>';
164
165
        $wasWriteSuccessful = fwrite($worksheet->getFilePointer(), $rowXML);
166
        if ($wasWriteSuccessful === false) {
167
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
168
        }
169
    }
170
171
    /**
172
     * Build and return xml for a single cell.
173
     *
174
     * @param int $rowIndex
175
     * @param int $cellNumber
176
     * @param Cell $cell
177
     * @param int $styleId
178
     * @return string
179
     * @throws InvalidArgumentException If the given value cannot be processed
180
     */
181
    private function getCellXML($rowIndex, $cellNumber, Cell $cell, $styleId)
182
    {
183
        $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
184
        $cellXML = '<c r="' . $columnIndex . $rowIndex . '"';
185
        $cellXML .= ' s="' . $styleId . '"';
186
187
        if ($cell->isString()) {
188
            $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell->getValue());
189
        } else if ($cell->isBoolean()) {
190
            $cellXML .= ' t="b"><v>' . intval($cell->getValue()) . '</v></c>';
191
        } else if ($cell->isNumeric()) {
192
            $cellXML .= '><v>' . $cell->getValue() . '</v></c>';
193
        } else if ($cell->isEmpty()) {
194
            if ($this->styleManager->shouldApplyStyleOnEmptyCell($styleId)) {
195
                $cellXML .= '/>';
196
            } else {
197
                // don't write empty cells that do no need styling
198
                // NOTE: not appending to $cellXML is the right behavior!!
199
                $cellXML = '';
200
            }
201
        } else {
202
            throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cell->getValue()));
203
        }
204
205
        return $cellXML;
206
    }
207
208
    /**
209
     * Returns the XML fragment for a cell containing a non empty string
210
     *
211
     * @param string $cellValue The cell value
212
     * @return string The XML fragment representing the cell
213
     * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell
214
     */
215
    private function getCellXMLFragmentForNonEmptyString($cellValue)
216
    {
217
        if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) {
218
            throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)');
219
        }
220
221
        if ($this->shouldUseInlineStrings) {
222
            $cellXMLFragment = ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
223
        } else {
224
            $sharedStringId = $this->sharedStringsManager->writeString($cellValue);
225
            $cellXMLFragment = ' t="s"><v>' . $sharedStringId . '</v></c>';
226
        }
227
228
        return $cellXMLFragment;
229
    }
230
231
    /**
232
     * Closes the worksheet
233
     *
234
     * @param Worksheet $worksheet
235
     * @return void
236
     */
237 4
    public function close(Worksheet $worksheet)
238
    {
239 4
        $worksheetFilePointer = $worksheet->getFilePointer();
240
241 4
        if (!is_resource($worksheetFilePointer)) {
242
            return;
243
        }
244
245 4
        fwrite($worksheetFilePointer, '</sheetData>');
246 4
        fwrite($worksheetFilePointer, '</worksheet>');
247 4
        fclose($worksheetFilePointer);
248
    }
249
}