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

WorksheetManager::addNonEmptyRow()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 3.0021

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 26
ccs 15
cts 16
cp 0.9375
rs 8.8571
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\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
     */
66 43
    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
    ) {
74 43
        $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS);
75 43
        $this->styleManager = $styleManager;
76 43
        $this->sharedStringsManager = $sharedStringsManager;
77 43
        $this->stringsEscaper = $stringsEscaper;
78 43
        $this->stringHelper = $stringHelper;
79 43
        $this->entityFactory = $entityFactory;
80 43
    }
81
82
    /**
83
     * @return SharedStringsManager
84
     */
85 35
    public function getSharedStringsManager()
86
    {
87 35
        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
     */
97 43
    public function startSheet(Worksheet $worksheet)
98
    {
99 43
        $sheetFilePointer = fopen($worksheet->getFilePath(), 'w');
100 43
        $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
101
102 43
        $worksheet->setFilePointer($sheetFilePointer);
103
104 43
        fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER);
105 43
        fwrite($sheetFilePointer, '<sheetData>');
106 43
    }
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
     */
115 43
    private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
116
    {
117 43
        if (!$sheetFilePointer) {
118
            throw new IOException('Unable to open sheet for writing.');
119
        }
120 43
    }
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 31
    public function addRow(Worksheet $worksheet, Row $row)
133
    {
134 31
        if (!$row->isEmpty()) {
135 31
            $this->addNonEmptyRow($worksheet, $row);
136
        }
137
138 29
        $worksheet->setLastWrittenRowIndex($worksheet->getLastWrittenRowIndex() + 1);
139 29
    }
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
     * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
149
     * @return void
150
     */
151 31
    private function addNonEmptyRow(Worksheet $worksheet, Row $row)
152
    {
153 31
        $cellNumber = 0;
154 31
        $rowIndex = $worksheet->getLastWrittenRowIndex() + 1;
155 31
        $numCells = count($row->getCells());
156
157 31
        $rowXML = '<row r="' . $rowIndex . '" spans="1:' . $numCells . '">';
158
159
        // @TODO refactoring: move this to its own method
160
        /** @var Cell $cell */
161 31
        foreach($row->getCells() as $cell) {
162
            // Apply styles - the row style is merged at this point
163 31
            $cell->applyStyle($row->getStyle());
164 31
            $this->styleManager->applyExtraStylesIfNeeded($cell);
165 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...
166 31
            $rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cell, $registeredStyle->getId());
167 29
            $cellNumber++;
168
        }
169
170 29
        $rowXML .= '</row>';
171
172 29
        $wasWriteSuccessful = fwrite($worksheet->getFilePointer(), $rowXML);
173 29
        if ($wasWriteSuccessful === false) {
174
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
175
        }
176 29
    }
177
178
    /**
179
     * Build and return xml for a single cell.
180
     *
181
     * @param int $rowIndex
182
     * @param int $cellNumber
183
     * @param Cell $cell
184
     * @param int $styleId
185
     * @throws InvalidArgumentException If the given value cannot be processed
186
     * @return string
187
     */
188 31
    private function getCellXML($rowIndex, $cellNumber, Cell $cell, $styleId)
189
    {
190 31
        $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
191 31
        $cellXML = '<c r="' . $columnIndex . $rowIndex . '"';
192 31
        $cellXML .= ' s="' . $styleId . '"';
193
194 31
        if ($cell->isString()) {
195 30
            $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell->getValue());
196 4
        } elseif ($cell->isBoolean()) {
197 2
            $cellXML .= ' t="b"><v>' . (int) ($cell->getValue()) . '</v></c>';
198 4
        } elseif ($cell->isNumeric()) {
199 2
            $cellXML .= '><v>' . $cell->getValue() . '</v></c>';
200 3
        } elseif ($cell->isEmpty()) {
201 2
            if ($this->styleManager->shouldApplyStyleOnEmptyCell($styleId)) {
202 1
                $cellXML .= '/>';
203
            } else {
204
                // don't write empty cells that do no need styling
205
                // NOTE: not appending to $cellXML is the right behavior!!
206 2
                $cellXML = '';
207
            }
208
        } else {
209 1
            throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cell->getValue()));
210
        }
211
212 29
        return $cellXML;
213
    }
214
215
    /**
216
     * Returns the XML fragment for a cell containing a non empty string
217
     *
218
     * @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 30
    private function getCellXMLFragmentForNonEmptyString($cellValue)
223
    {
224 30
        if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) {
225 1
            throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)');
226
        }
227
228 29
        if ($this->shouldUseInlineStrings) {
229 24
            $cellXMLFragment = ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
230
        } else {
231 5
            $sharedStringId = $this->sharedStringsManager->writeString($cellValue);
232 5
            $cellXMLFragment = ' t="s"><v>' . $sharedStringId . '</v></c>';
233
        }
234
235 29
        return $cellXMLFragment;
236
    }
237
238
    /**
239
     * Closes the worksheet
240
     *
241
     * @param Worksheet $worksheet
242
     * @return void
243
     */
244 35
    public function close(Worksheet $worksheet)
245
    {
246 35
        $worksheetFilePointer = $worksheet->getFilePointer();
247
248 35
        if (!is_resource($worksheetFilePointer)) {
249
            return;
250
        }
251
252 35
        fwrite($worksheetFilePointer, '</sheetData>');
253 35
        fwrite($worksheetFilePointer, '</worksheet>');
254 35
        fclose($worksheetFilePointer);
255 35
    }
256
}
257