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

WorksheetManager::addNonEmptyRow()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 3.0021

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 15
cts 16
cp 0.9375
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 15
nc 4
nop 2
crap 3.0021
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 46
    public function __construct(
63
        OptionsManagerInterface $optionsManager,
64
        StyleManager $styleManager,
65
        SharedStringsManager $sharedStringsManager,
66
        \Box\Spout\Common\Escaper\XLSX $stringsEscaper,
67
        StringHelper $stringHelper)
68
    {
69 46
        $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS);
70 46
        $this->styleManager = $styleManager;
71 46
        $this->sharedStringsManager = $sharedStringsManager;
72 46
        $this->stringsEscaper = $stringsEscaper;
73 46
        $this->stringHelper = $stringHelper;
74 46
    }
75
76
    /**
77
     * @return SharedStringsManager
78
     */
79 36
    public function getSharedStringsManager()
80
    {
81 36
        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 46
    public function startSheet(Worksheet $worksheet)
93
    {
94 46
        $sheetFilePointer = fopen($worksheet->getFilePath(), 'w');
95 46
        $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
96
97 46
        $worksheet->setFilePointer($sheetFilePointer);
98
99 46
        fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER);
100 46
        fwrite($sheetFilePointer, '<sheetData>');
101 46
    }
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 46
    private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
111
    {
112 46
        if (!$sheetFilePointer) {
113
            throw new IOException('Unable to open sheet for writing.');
114
        }
115 46
    }
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 33
    public function addRow(Worksheet $worksheet, Row $row)
127
    {
128 33
        if (!$row->isEmpty()) {
129 33
            $this->addNonEmptyRow($worksheet, $row);
130
        }
131
132 31
        $worksheet->setLastWrittenRowIndex($worksheet->getLastWrittenRowIndex() + 1);
133 31
    }
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 33
    private function addNonEmptyRow(Worksheet $worksheet, Row $row)
145
    {
146 33
        $cellNumber = 0;
147 33
        $rowIndex = $worksheet->getLastWrittenRowIndex() + 1;
148 33
        $numCells = count($row->getCells());
149
150 33
        $rowXML = '<row r="' . $rowIndex . '" spans="1:' . $numCells . '">';
151
152
        /** @var Cell $cell */
153 33
        foreach($row->getCells() as $cell) {
154
            // Apply styles - the row style is merged at this point
155 33
            $cell->applyStyle($row->getStyle());
156 33
            $this->styleManager->applyExtraStylesIfNeeded($cell);
157 33
            $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...
158 33
            $rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cell, $registeredStyle->getId());
159 31
            $cellNumber++;
160
        }
161
162 31
        $rowXML .= '</row>';
163
164 31
        $wasWriteSuccessful = fwrite($worksheet->getFilePointer(), $rowXML);
165 31
        if ($wasWriteSuccessful === false) {
166
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
167
        }
168 31
    }
169
170
    /**
171
     * Build and return xml for a single cell.
172
     *
173
     * @param int $rowIndex
174
     * @param int $cellNumber
175
     * @param Cell $cell
176
     * @param int $styleId
177
     * @return string
178
     * @throws InvalidArgumentException If the given value cannot be processed
179
     */
180 33
    private function getCellXML($rowIndex, $cellNumber, Cell $cell, $styleId)
181
    {
182 33
        $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
183 33
        $cellXML = '<c r="' . $columnIndex . $rowIndex . '"';
184 33
        $cellXML .= ' s="' . $styleId . '"';
185
186 33
        if ($cell->isString()) {
187 32
            $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell->getValue());
188 5
        } else if ($cell->isBoolean()) {
189 2
            $cellXML .= ' t="b"><v>' . intval($cell->getValue()) . '</v></c>';
190 5
        } else if ($cell->isNumeric()) {
191 2
            $cellXML .= '><v>' . $cell->getValue() . '</v></c>';
192 4
        } else if ($cell->isEmpty()) {
193 2
            if ($this->styleManager->shouldApplyStyleOnEmptyCell($styleId)) {
194 1
                $cellXML .= '/>';
195
            } else {
196
                // don't write empty cells that do no need styling
197
                // NOTE: not appending to $cellXML is the right behavior!!
198 2
                $cellXML = '';
199
            }
200
        } else {
201 2
            throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cell->getValue()));
202
        }
203
204 31
        return $cellXML;
205
    }
206
207
    /**
208
     * Returns the XML fragment for a cell containing a non empty string
209
     *
210
     * @param string $cellValue The cell value
211
     * @return string The XML fragment representing the cell
212
     * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell
213
     */
214 32
    private function getCellXMLFragmentForNonEmptyString($cellValue)
215
    {
216 32
        if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) {
217 1
            throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)');
218
        }
219
220 31
        if ($this->shouldUseInlineStrings) {
221 26
            $cellXMLFragment = ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
222
        } else {
223 5
            $sharedStringId = $this->sharedStringsManager->writeString($cellValue);
224 5
            $cellXMLFragment = ' t="s"><v>' . $sharedStringId . '</v></c>';
225
        }
226
227 31
        return $cellXMLFragment;
228
    }
229
230
    /**
231
     * Closes the worksheet
232
     *
233
     * @param Worksheet $worksheet
234
     * @return void
235
     */
236 36
    public function close(Worksheet $worksheet)
237
    {
238 36
        $worksheetFilePointer = $worksheet->getFilePointer();
239
240 36
        if (!is_resource($worksheetFilePointer)) {
241
            return;
242
        }
243
244 36
        fwrite($worksheetFilePointer, '</sheetData>');
245 36
        fwrite($worksheetFilePointer, '</worksheet>');
246 36
        fclose($worksheetFilePointer);
247
    }
248
}