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

getCellXMLFragmentForNonEmptyString()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

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