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

WorksheetManager   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 226
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 95.89%

Importance

Changes 0
Metric Value
wmc 21
lcom 1
cbo 10
dl 0
loc 226
ccs 70
cts 73
cp 0.9589
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 1
A getSharedStringsManager() 0 4 1
A startSheet() 0 10 1
A throwIfSheetFilePointerIsNotAvailable() 0 6 2
A addRow() 0 8 2
B addNonEmptyRow() 0 25 3
B getCellXML() 0 26 6
A getCellXMLFragmentForNonEmptyString() 0 15 3
A close() 0 12 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
        /** @var Cell $cell */
153 31
        foreach($row->getCells() as $cell) {
154
            // Apply styles - the row style is merged at this point
155 31
            $cell->applyStyle($row->getStyle());
156 31
            $this->styleManager->applyExtraStylesIfNeeded($cell);
157 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...
158 31
            $rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cell, $registeredStyle->getId());
159 29
            $cellNumber++;
160
        }
161
162 29
        $rowXML .= '</row>';
163
164 29
        $wasWriteSuccessful = fwrite($worksheet->getFilePointer(), $rowXML);
165 29
        if ($wasWriteSuccessful === false) {
166
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
167
        }
168 29
    }
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 31
    private function getCellXML($rowIndex, $cellNumber, Cell $cell, $styleId)
181
    {
182 31
        $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
183 31
        $cellXML = '<c r="' . $columnIndex . $rowIndex . '"';
184 31
        $cellXML .= ' s="' . $styleId . '"';
185
186 31
        if ($cell->isString()) {
187 30
            $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell->getValue());
188 4
        } else if ($cell->isBoolean()) {
189 2
            $cellXML .= ' t="b"><v>' . intval($cell->getValue()) . '</v></c>';
190 4
        } else if ($cell->isNumeric()) {
191 2
            $cellXML .= '><v>' . $cell->getValue() . '</v></c>';
192 3
        } 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 1
            throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cell->getValue()));
202
        }
203
204 29
        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 30
    private function getCellXMLFragmentForNonEmptyString($cellValue)
215
    {
216 30
        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 29
        if ($this->shouldUseInlineStrings) {
221 24
            $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 29
        return $cellXMLFragment;
228
    }
229
230
    /**
231
     * Closes the worksheet
232
     *
233
     * @param Worksheet $worksheet
234
     * @return void
235
     */
236 35
    public function close(Worksheet $worksheet)
237
    {
238 35
        $worksheetFilePointer = $worksheet->getFilePointer();
239
240 35
        if (!is_resource($worksheetFilePointer)) {
241
            return;
242
        }
243
244 35
        fwrite($worksheetFilePointer, '</sheetData>');
245 35
        fwrite($worksheetFilePointer, '</worksheet>');
246 35
        fclose($worksheetFilePointer);
247
    }
248
}