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

WorksheetManager::isEmptyRow()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 1
crap 2
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 43
    public function __construct(
63
        OptionsManagerInterface $optionsManager,
64
        StyleManager $styleManager,
65
        SharedStringsManager $sharedStringsManager,
66
        \Box\Spout\Common\Escaper\XLSX $stringsEscaper,
67
        StringHelper $stringHelper)
68
    {
69 43
        $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS);
70 43
        $this->styleManager = $styleManager;
71 43
        $this->sharedStringsManager = $sharedStringsManager;
72 43
        $this->stringsEscaper = $stringsEscaper;
73 43
        $this->stringHelper = $stringHelper;
74 43
    }
75
76
    /**
77
     * @return SharedStringsManager
78
     */
79 35
    public function getSharedStringsManager()
80
    {
81 35
        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 43
    public function startSheet(Worksheet $worksheet)
93
    {
94 43
        $sheetFilePointer = fopen($worksheet->getFilePath(), 'w');
95 43
        $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
96
97 43
        $worksheet->setFilePointer($sheetFilePointer);
98
99 43
        fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER);
100 43
        fwrite($sheetFilePointer, '<sheetData>');
101 43
    }
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 43
    private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
111
    {
112 43
        if (!$sheetFilePointer) {
113
            throw new IOException('Unable to open sheet for writing.');
114
        }
115 43
    }
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 31
    public function addRow(Worksheet $worksheet, Row $row)
127
    {
128 31
        if (!$row->isEmpty()) {
129 31
            $this->addNonEmptyRow($worksheet, $row);
130
        }
131
132 29
        $worksheet->setLastWrittenRowIndex($worksheet->getLastWrittenRowIndex() + 1);
133 29
    }
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 31
    private function addNonEmptyRow(Worksheet $worksheet, Row $row)
145
    {
146 31
        $cellNumber = 0;
147 31
        $rowIndex = $worksheet->getLastWrittenRowIndex() + 1;
148 31
        $numCells = count($row->getCells());
149
150 31
        $rowXML = '<row r="' . $rowIndex . '" spans="1:' . $numCells . '">';
151
152
        // @TODO refactoring: move this to its own method
153
        /** @var Cell $cell */
154 31
        foreach($row->getCells() as $cell) {
155
            // Apply styles - the row style is merged at this point
156 31
            $cell->applyStyle($row->getStyle());
157 31
            $this->styleManager->applyExtraStylesIfNeeded($cell);
158 31
            $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 31
            $rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cell, $registeredStyle->getId());
160 29
            $cellNumber++;
161
        }
162
163 29
        $rowXML .= '</row>';
164
165 29
        $wasWriteSuccessful = fwrite($worksheet->getFilePointer(), $rowXML);
166 29
        if ($wasWriteSuccessful === false) {
167
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
168
        }
169 29
    }
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 31
    private function getCellXML($rowIndex, $cellNumber, Cell $cell, $styleId)
182
    {
183 31
        $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
184 31
        $cellXML = '<c r="' . $columnIndex . $rowIndex . '"';
185 31
        $cellXML .= ' s="' . $styleId . '"';
186
187 31
        if ($cell->isString()) {
188 30
            $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell->getValue());
189 4
        } else if ($cell->isBoolean()) {
190 2
            $cellXML .= ' t="b"><v>' . intval($cell->getValue()) . '</v></c>';
191 4
        } else if ($cell->isNumeric()) {
192 2
            $cellXML .= '><v>' . $cell->getValue() . '</v></c>';
193 3
        } else if ($cell->isEmpty()) {
194 2
            if ($this->styleManager->shouldApplyStyleOnEmptyCell($styleId)) {
195 1
                $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 2
                $cellXML = '';
200
            }
201
        } else {
202 1
            throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cell->getValue()));
203
        }
204
205 29
        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 30
    private function getCellXMLFragmentForNonEmptyString($cellValue)
216
    {
217 30
        if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) {
218 1
            throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)');
219
        }
220
221 29
        if ($this->shouldUseInlineStrings) {
222 24
            $cellXMLFragment = ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
223
        } else {
224 5
            $sharedStringId = $this->sharedStringsManager->writeString($cellValue);
225 5
            $cellXMLFragment = ' t="s"><v>' . $sharedStringId . '</v></c>';
226
        }
227
228 29
        return $cellXMLFragment;
229
    }
230
231
    /**
232
     * Closes the worksheet
233
     *
234
     * @param Worksheet $worksheet
235
     * @return void
236
     */
237 35
    public function close(Worksheet $worksheet)
238
    {
239 35
        $worksheetFilePointer = $worksheet->getFilePointer();
240
241 35
        if (!is_resource($worksheetFilePointer)) {
242
            return;
243
        }
244
245 35
        fwrite($worksheetFilePointer, '</sheetData>');
246 35
        fwrite($worksheetFilePointer, '</worksheet>');
247 35
        fclose($worksheetFilePointer);
248
    }
249
}