Test Setup Failed
Pull Request — develop_3.0 (#434)
by Hura
04:56
created

WorksheetManager::addNonEmptyRow()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 9
cts 9
cp 1
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 15
nc 4
nop 2
crap 3
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\Common\Manager\OptionsManagerInterface;
9
use Box\Spout\Writer\Common\Creator\EntityFactory;
10
use Box\Spout\Writer\Common\Entity\Cell;
11
use Box\Spout\Writer\Common\Entity\Options;
12
use Box\Spout\Writer\Common\Entity\Style\Style;
13
use Box\Spout\Writer\Common\Entity\Row;
14
use Box\Spout\Writer\Common\Entity\Worksheet;
15
use Box\Spout\Writer\Common\Helper\CellHelper;
16
use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface;
17
use Box\Spout\Writer\XLSX\Manager\Style\StyleManager;
18
19
/**
20
 * Class WorksheetManager
21
 * XLSX worksheet manager, providing the interfaces to work with XLSX worksheets.
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\Helper\Escaper\XLSX Strings escaper */
48
    private $stringsEscaper;
49
50
    /** @var StringHelper String helper */
51
    private $stringHelper;
52
53
    /** @var EntityFactory Factory to create entities */
54
    private $entityFactory;
55
56
    /**
57
     * WorksheetManager constructor.
58
     *
59
     * @param OptionsManagerInterface $optionsManager
60
     * @param StyleManager $styleManager
61
     * @param SharedStringsManager $sharedStringsManager
62
     * @param \Box\Spout\Common\Helper\Escaper\XLSX $stringsEscaper
63
     * @param StringHelper $stringHelper
64
     * @param EntityFactory $entityFactory
65 46
     */
66
    public function __construct(
67
        OptionsManagerInterface $optionsManager,
68
        StyleManager $styleManager,
69
        SharedStringsManager $sharedStringsManager,
70
        \Box\Spout\Common\Helper\Escaper\XLSX $stringsEscaper,
71
        StringHelper $stringHelper,
72
        EntityFactory $entityFactory
73 46
    ) {
74 46
        $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS);
75 46
        $this->styleManager = $styleManager;
76 46
        $this->sharedStringsManager = $sharedStringsManager;
77 46
        $this->stringsEscaper = $stringsEscaper;
78 46
        $this->stringHelper = $stringHelper;
79 46
        $this->entityFactory = $entityFactory;
80
    }
81
82
    /**
83
     * @return SharedStringsManager
84 36
     */
85
    public function getSharedStringsManager()
86 36
    {
87
        return $this->sharedStringsManager;
88
    }
89
90
    /**
91
     * Prepares the worksheet to accept data
92
     *
93
     * @param Worksheet $worksheet The worksheet to start
94
     * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
95
     * @return void
96 46
     */
97
    public function startSheet(Worksheet $worksheet)
98 46
    {
99 46
        $sheetFilePointer = fopen($worksheet->getFilePath(), 'w');
100
        $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
101 46
102
        $worksheet->setFilePointer($sheetFilePointer);
103 46
104 46
        fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER);
105 46
        fwrite($sheetFilePointer, '<sheetData>');
106
    }
107
108
    /**
109
     * Checks if the sheet has been sucessfully created. Throws an exception if not.
110
     *
111
     * @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file
112
     * @throws IOException If the sheet data file cannot be opened for writing
113
     * @return void
114 46
     */
115
    private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
116 46
    {
117
        if (!$sheetFilePointer) {
118
            throw new IOException('Unable to open sheet for writing.');
119 46
        }
120
    }
121
122
    /**
123
     * Adds a row to the worksheet.
124
     *
125
     * @param Worksheet $worksheet The worksheet to add the row to
126
     * @param Row $row The row to be added
127
     * @return void
128
     * @throws IOException If the data cannot be written
129
     * @throws InvalidArgumentException If a cell value's type is not supported
130
     * @return void
131
     */
132 33
    public function addRow(Worksheet $worksheet, Row $row)
133
    {
134 33
        if (!$row->isEmpty()) {
135 33
            $this->addNonEmptyRow($worksheet, $row);
136
        }
137
138 31
        $worksheet->setLastWrittenRowIndex($worksheet->getLastWrittenRowIndex() + 1);
139 31
    }
140
141
    /**
142
     * Adds non empty row to the worksheet.
143
     *
144
     * @param Row $row The row to be written
145
     * @return void
146
     *
147
     * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
148 33
     * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
149
     * @return void
150 33
     */
151
    private function addNonEmptyRow(Worksheet $worksheet, Row $row)
152 33
    {
153
        $cellNumber = 0;
154
        $rowIndex = $worksheet->getLastWrittenRowIndex() + 1;
155
        $numCells = count($row->getCells());
156
157
        $rowXML = '<row r="' . $rowIndex . '" spans="1:' . $numCells . '">';
158
159
        // @TODO refactoring: move this to its own method
160
        /** @var Cell $cell */
161
        foreach($row->getCells() as $cell) {
162
            // Apply styles - the row style is merged at this point
163
            $cell->applyStyle($row->getStyle());
164
            $this->styleManager->applyExtraStylesIfNeeded($cell);
165
            $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...
166 33
            $rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cell, $registeredStyle->getId());
167
            $cellNumber++;
168 33
        }
169 33
170 33
        $rowXML .= '</row>';
171
172 33
        $wasWriteSuccessful = fwrite($worksheet->getFilePointer(), $rowXML);
173
        if ($wasWriteSuccessful === false) {
174 33
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
175 33
        }
176 31
    }
177
178
    /**
179 31
     * Build and return xml for a single cell.
180
     *
181 31
     * @param int $rowIndex
182 31
     * @param int $cellNumber
183
     * @param Cell $cell
184
     * @param int $styleId
185 31
     * @throws InvalidArgumentException If the given value cannot be processed
186
     * @return string
187
     */
188
    private function getCellXML($rowIndex, $cellNumber, Cell $cell, $styleId)
189
    {
190
        $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
191
        $cellXML = '<c r="' . $columnIndex . $rowIndex . '"';
192
        $cellXML .= ' s="' . $styleId . '"';
193
194
        if ($cell->isString()) {
195
            $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell->getValue());
196
        } elseif ($cell->isBoolean()) {
197 33
            $cellXML .= ' t="b"><v>' . (int) ($cell->getValue()) . '</v></c>';
198
        } elseif ($cell->isNumeric()) {
199 33
            $cellXML .= '><v>' . $cell->getValue() . '</v></c>';
200 33
        } elseif ($cell->isEmpty()) {
201 33
            if ($this->styleManager->shouldApplyStyleOnEmptyCell($styleId)) {
202
                $cellXML .= '/>';
203
            } else {
204 33
                // don't write empty cells that do no need styling
205 2
                // NOTE: not appending to $cellXML is the right behavior!!
206
                $cellXML = '';
207 31
            }
208
        } else {
209
            throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cell->getValue()));
210 33
        }
211 32
212 5
        return $cellXML;
213 2
    }
214 5
215 2
    /**
216 4
     * Returns the XML fragment for a cell containing a non empty string
217 2
     *
218 1
     * @param string $cellValue The cell value
219
     * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell
220
     * @return string The XML fragment representing the cell
221
     */
222 2
    private function getCellXMLFragmentForNonEmptyString($cellValue)
223
    {
224
        if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) {
225 2
            throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)');
226
        }
227
228 31
        if ($this->shouldUseInlineStrings) {
229
            $cellXMLFragment = ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
230
        } else {
231
            $sharedStringId = $this->sharedStringsManager->writeString($cellValue);
232
            $cellXMLFragment = ' t="s"><v>' . $sharedStringId . '</v></c>';
233
        }
234
235
        return $cellXMLFragment;
236
    }
237
238 32
    /**
239
     * Closes the worksheet
240 32
     *
241 1
     * @param Worksheet $worksheet
242
     * @return void
243
     */
244 31
    public function close(Worksheet $worksheet)
245 26
    {
246
        $worksheetFilePointer = $worksheet->getFilePointer();
247 5
248 5
        if (!is_resource($worksheetFilePointer)) {
249
            return;
250
        }
251 31
252
        fwrite($worksheetFilePointer, '</sheetData>');
253
        fwrite($worksheetFilePointer, '</worksheet>');
254
        fclose($worksheetFilePointer);
255
    }
256
}
257